# JuLC — Full Documentation (concatenated for AI ingestion) > Single-file dump of the JuLC docsite, suitable for AI agent ingestion. julcVersion: 0.1.0-pre12 Generated: 2026-05-17T08:09:51.448Z Site: https://julc.dev Repo: https://github.com/bloxbean/julc Examples: https://github.com/bloxbean/julc-examples ## Table of Contents - [AI → Using JuLC with AI Agents](https://julc.dev/ai/) - [AI → JuLC AI Starter Pack](https://julc.dev/ai/starter-pack/) - [AI → MCP closed-loop walkthrough](https://julc.dev/ai/transcripts/closed-loop-walkthrough/) - [Overview → JuLC Documentation](https://julc.dev/overview/) - [Getting Started → Write Your First JuLC Contract](https://julc.dev/first-contract/) - [Getting Started → Getting Started with JuLC](https://julc.dev/getting-started/) - [Guides → JuLC Advanced Guide: Low-Level Patterns](https://julc.dev/guides/advanced-guide/) - [Guides → For-Each Loop Patterns](https://julc.dev/guides/for-loop-patterns/) - [Guides → Testing Guide](https://julc.dev/guides/testing-guide/) - [Guides → Source Maps — Runtime Error Location Reporting](https://julc.dev/guides/source-maps/) - [Standard Library → JuLC Standard Library Usage Guide](https://julc.dev/stdlib/stdlib-guide/) - [Reference → JuLC API Reference](https://julc.dev/reference/api-reference/) - [Reference → JuLC Library Developer Guide](https://julc.dev/reference/library-developer-guide/) - [Reference → Examples](https://julc.dev/reference/examples/) - [Reference → JuLC Compiler Troubleshooting Guide](https://julc.dev/reference/troubleshooting/) - [Internals → JuLC Compiler Design](https://julc.dev/internals/compiler-design/) - [Internals → Java to UPLC End to End](https://julc.dev/internals/java-to-uplc-end-to-end/) - [Internals → JuLC Compiler Developer Guide](https://julc.dev/internals/compiler-developer-guide/) - [Experimental → JRL (JuLC Rule Language) Guide (Experimental)](https://julc.dev/experimental/jrl-guide/) --- # AI --- ## Using JuLC with AI Agents Source: https://julc.dev/ai/ > How to use Claude Code, Cursor, Continue, ChatGPT, and other AI tools to write JuLC smart contracts. JuLC is designed to be **AI-native**: a Java developer working with any AI agent can be productive in JuLC within minutes, with no JuLC training data required. This page is the entry point for using JuLC with AI tools. #### TL;DR The single best thing to do is point your AI agent at the **[AI Starter Pack](/ai/starter-pack/)** or the full docs dump: | File | When to use it | |---|---| | **[`/ai/starter-pack/`](/ai/starter-pack/)** | The single highest-leverage artifact. Distills what AI needs to write correct JuLC on first try (limitations, anti-patterns, idioms, error→fix table, canonical examples). ~15–40K tokens. | | **[`/llms.txt`](/llms.txt)** | Curated index of the JuLC docsite, following [llmstxt.org](https://llmstxt.org/). Small, agent-friendly. | | **[`/llms-full.txt`](/llms-full.txt)** | Full JuLC docsite concatenated as a single markdown file. Ingest this for full coverage (~50–150K tokens). | #### Per-tool setup ##### Claude Code (CLI) Drop a `CLAUDE.md` into the root of your JuLC project: ```bash curl -o CLAUDE.md https://julc.dev/ai/starter-pack.md ``` Claude Code automatically reads `CLAUDE.md` at the start of every session, so the agent always has JuLC context. For multi-project setups, you can also reference the hosted version from your global `~/.claude/CLAUDE.md`: ```markdown When working in a JuLC project, follow the rules at https://julc.dev/ai/starter-pack/ ``` ##### Cursor Add a project-level rule at `.cursor/rules/julc.mdc`: ```bash mkdir -p .cursor/rules curl -o .cursor/rules/julc.mdc https://julc.dev/ai/starter-pack.md ``` Cursor automatically applies rules in `.cursor/rules/` when working in the project. ##### Continue (VS Code / JetBrains) Add a context provider in `.continue/config.json`: ```json { "contextProviders": [ { "name": "url", "params": { "url": "https://julc.dev/llms-full.txt" } } ] } ``` ##### ChatGPT / Claude.ai (web) For one-off conversations, paste this at the start of your chat: ``` I'm writing Cardano smart contracts in JuLC (Java → Plutus V3 UPLC compiler). Read the JuLC AI Starter Pack at https://julc.dev/ai/starter-pack and follow its rules strictly. In particular: - Always prefer high-level type classes (TxOut, Value, Address, sealed interfaces, JulcList, JulcMap) over raw PlutusData.ConstrData/ IntData/BytesData/MapData/ListData. - The Java subset is restricted: no mutable variables, no lambda .apply(), no return inside while loops, no reflection. ``` For long-lived projects, attach the full docs dump (`https://julc.dev/llms-full.txt`) to your project files / custom GPT. ##### Codex / Aider / other AI-coding tools Most tools support project-level context files. Drop `https://julc.dev/ai/starter-pack.md` into your tool's equivalent of "system prompt" or "always include" file list. #### JuLC MCP server The `julc mcp` subcommand runs an MCP server over stdio that exposes the JuLC compiler, linter, and evaluator as tools. Once configured, any MCP-compatible agent (Claude Code, Claude Desktop, Cursor, Continue, …) can compile, lint, and evaluate JuLC code directly — closing the loop so the AI recovers from its own errors without copy-pasting between you and the compiler. ##### Install `julc` first ```bash ### macOS / Linux brew install bloxbean/tap/julc julc --version julc mcp --help ``` If `julc --version` works, you're good. The `mcp` subcommand uses stdio — no port to configure. ##### Tools exposed The MCP server registers 13 tools (closed-loop compiler/evaluator tools, discovery tools, testkit, and conservative cost estimates), 6 read-only resources, 4 resource templates, and 4 prompt templates. The full list is also live at the `julc://server-info.md` MCP resource. **Closed compile-feedback loop (Phase C):** | Tool | Purpose | |---|---| | `julc_ping` | Round-trip check — returns the JuLC version. | | `julc_compile` | Compiles JuLC source. Returns structured diagnostics with stable `JULC####` codes, UPLC, and script size. Accepts optional `librarySources` for `@OnchainLibrary` helpers. | | `julc_lint` | Pre-compile static analysis. 15 rules covering switch-field shadow, raw `PlutusData` construction, `Optional.mkSome`/`mkNone` (which doesn't exist!), double `.hash()`, mutation, return-in-loop, lambda `.apply()`, banned `@Param` types, `Tuple2` switches, and more. | | `julc_evaluate` | Compile + run a single static method on the JuLC VM. Accepts recursive PlutusData args (`{int}`, `{bytes}`, `{string}`, `{bool}`, `{unit}`, `{constr:{tag,fields}}`, `{list:[…]}`, `{map:[{key,value}]}`). Returns the extracted result + CPU/memory budget. | | `julc_estimate_costs` | Conservative estimate: compile source for script size; if `method` + `args` are supplied, delegates to VM method evaluation for CPU/memory. Not a full transaction-context benchmark. | **Discovery and testkit (Phase D):** | Tool | Purpose | |---|---| | `julc_stdlib_list` | Enumerate every stdlib library + its method count. | | `julc_stdlib_method` | Look up the signature, params, and return type of a single stdlib method. | | `julc_builtins_list` | List Plutus builtins exposed by `Builtins.*`. | | `julc_ledger_type` | Field/variant info for a ledger type (TxOut, Value, Address, sealed interfaces …). | | `julc_examples_search` | Tagged search across the canonical examples corpus. | | `julc_example_get` | Full source + commentary for a single example. | | `julc_explain_diagnostic` | `JULC####` code → root cause + canonical fix. | | `julc_test` | Run `@Test public static boolean` methods in a test source file; reports per-test pass/fail + CPU/mem budget. | **Read-only resources** (URIs accessible via `resources/read`): `julc://limitations.md`, `julc://diagnostics.json`, `julc://stdlib.json`, `julc://ledger.json`, `julc://builtins.json`, `julc://server-info.md`. **Resource templates**: `julc://stdlib/{lib}`, `julc://stdlib/{lib}/{method}`, `julc://ledger/{type}`, `julc://examples/{id}`. **Prompt templates**: `write-validator`, `migrate-from-aiken`, `migrate-from-plinth`, `debug-compile-error`. ##### Claude Code The recommended path is the `claude mcp add` command, which writes the server into `~/.claude.json`: ```bash ### Local scope (current project only — default) claude mcp add --transport stdio julc -- julc mcp ### Or user scope (all your projects) claude mcp add --transport stdio --scope user julc -- julc mcp ``` Then verify with `/mcp` inside Claude Code — `julc` should appear with all 13 tools. For project-wide team sharing, use a checked-in `.mcp.json` at your repo root: ```json { "mcpServers": { "julc": { "type": "stdio", "command": "julc", "args": ["mcp"] } } } ``` ##### Claude Desktop Edit `claude_desktop_config.json` (Settings → Developer → Edit Config): ```json { "mcpServers": { "julc": { "command": "julc", "args": ["mcp"] } } } ``` Restart Claude Desktop. The 13 tools should appear in the tool picker. ##### Cursor `.cursor/mcp.json` (project-local) or `~/.cursor/mcp.json` (global): ```json { "mcpServers": { "julc": { "command": "julc", "args": ["mcp"] } } } ``` ##### Continue In `~/.continue/config.yaml` (or the equivalent `config.json`): ```yaml mcpServers: - name: julc command: julc args: [mcp] ``` ##### Verifying the install After configuring, ask the agent: > Run the `julc_ping` MCP tool and tell me what it returns. You should see `julc-mcp v — alive`. If the tool isn't visible, check that `julc` is on the PATH the agent sees (use `which julc` from the same shell that launched the agent). ##### Diagnostic codes vs. lint rule IDs Two identifier schemes flow through the MCP tools: - **`JULC####`** (e.g. `JULC0015`) — emitted by `julc_compile` and registered in [`/ai/diagnostics.json`](/ai/diagnostics.json). Look up canonical fixes there. - **`JULC-LINT-*`** (e.g. `JULC-LINT-OPTIONAL-API`) — emitted by `julc_lint`. The `suggestion` field on each lint finding is authoritative; lint rule IDs are intentionally NOT in `/ai/diagnostics.json` (they are static-analysis hints, not runtime diagnostics). Some lint findings ALSO carry a `JULC####` code in their `diagnostic` field when the rule overlaps with a runtime diagnostic (e.g. `JULC-LINT-SWITCH-SHADOW` ↔ `JULC0021`); use that code for the catalog lookup if present. #### Additional Integrations - **JuLC Claude Skill**: available in the repo at [`tools/claude-skill/julc`](https://github.com/bloxbean/julc/tree/main/tools/claude-skill/julc). It bundles the starter pack, JuLC slash-command workflows, and local MCP configuration. - **Hosted SSE MCP endpoint** at `mcp.julc.dev` is still planned for users who don't want a local install. Track progress in [ADR-020: AI-Ready Developer Experience](https://github.com/bloxbean/julc/blob/main/adr/020-ai-ready-developer-experience.md). #### Why this works JuLC is a young language: it has been renamed and released after most LLM training cutoffs, so models have no training data for it. Without context, an AI will hallucinate — generating code that looks like Plutus or plain Java but does not compile. The artifacts on this page solve that by **giving the agent everything it needs to know in one ingest**: - **What the Java subset actually allows** (and what it doesn't — no mutation, no lambda `.apply()`, no `return` in `while` loops, etc.). - **Stdlib API surface** with exact signatures, so the agent stops inventing methods that don't exist. - **Ledger types catalog** so the agent uses `TxOut`, `Value`, `Address`, sealed interfaces — not raw `PlutusData`. - **Known compiler limitations and gotchas** distilled from thousands of real test cases — the precise failure modes JuLC contributors have already debugged. - **Canonical examples** drawn from [`julc-examples`](https://github.com/bloxbean/julc-examples) (cftemplates, nft, uverify, mpf, linkedlist, swap, lending, validators). The result: a Java developer who has never seen JuLC can produce a working validator on first try, with the AI handling the JuLC-specific idioms. #### Feedback The starter pack is a living document. If your agent fails on a specific JuLC pattern, [open an issue](https://github.com/bloxbean/julc/issues) — the failure mode probably belongs in the starter pack so the next agent doesn't repeat it. --- ## JuLC AI Starter Pack Source: https://julc.dev/ai/starter-pack/ > Everything an AI agent needs to write correct JuLC on first try. Ingest this file before generating JuLC code. > **Read this entire document before generating any JuLC code.** It distills the JuLC subset of Java, the on-chain idioms, the compiler's known limitations, and the error patterns that AI agents most commonly get wrong. Following the rules here will save many compile-fail-retry cycles. This pack is **not** a tutorial for humans — it is optimized for AI ingestion. For the human-friendly tutorial, see [Write Your First Contract](/first-contract/). --- #### 1. What is JuLC? JuLC is a compiler from a **safe subset of Java 25** to **Plutus V3 UPLC** (Untyped Plutus Lambda Calculus), the on-chain execution language of Cardano. You write validators as ordinary Java classes — using `record`s, sealed interfaces, switch expressions, and `static` methods — and the JuLC compiler produces an on-chain script. Key facts: - **On-chain only.** JuLC code runs on the Cardano CEK machine. It is the validator/minting-policy code, not the off-chain transaction-building code. - **Off-chain Java** (using cardano-client-lib `0.7.1`) is a separate concern; you can call the *same* JVM classes (records) off-chain to construct datums and redeemers, but the bodies of `@OnchainLibrary` and validator methods compile to UPLC, not run on the JVM. - **Plutus V3 only** (Conway era). No V1/V2 support. - **The Java source is the authoring surface; UPLC is what runs.** Many "weird" rules below exist because UPLC has no notion of mutable state, no exceptions (except `error`), and no general recursion (only fixed-point combinators). #### 2. Project structure (what `julc new` produces) A typical JuLC project looks like: ``` my-contract/ ├── build.gradle # Gradle config with julc-annotation-processor ├── src/main/java/ │ └── com/example/ │ ├── MyValidator.java # @SpendingValidator class │ └── MyLib.java # @OnchainLibrary class (optional) └── src/test/java/ # julc-testkit tests ``` Build artifact: `build/classes/META-INF/plutus/MyValidator.plutus.json` — the compiled UPLC + CIP-57 blueprint, loadable off-chain via `JulcScriptLoader.load(MyValidator.class)`. #### 3. The Java subset that compiles to UPLC ##### 3.1 Allowed - **Static methods on classes.** Validator entrypoints and helpers are all `static`. - **`record`s** as datum/redeemer types. Records are compiled to `ConstrData` with field access compiled to `unConstrData → indexed access`. - **Sealed interfaces with permitted records** as ADTs (algebraic data types). Compiled to `ConstrData` with the constructor index discriminating variants. - **`switch` expressions** over sealed interfaces (pattern matching on permitted records). The compiler **requires exhaustiveness** unless a `default ->` branch is present. - **`if` / `else if` / `else`**, **ternary `? :`**. - **`for-each` loops** over `JulcList`, `JulcMap`, and ledger list types. Auto-desugared to recursive UPLC. - **`while` loops** with **explicit accumulators** (single or multi-variable). Desugared to `LetRec` fixed points. - **Local variables** declared with `var` or explicit type, **must be initialized at declaration**, and **are immutable after assignment** (single-assignment). - **Method calls** to `@OnchainLibrary` static methods, stdlib (`ContextsLib`, `ListsLib`, etc.), instance methods on `JulcList`/`JulcMap`/`String`/`byte[]`/`BigInteger`, and `Builtins.*` (Plutus builtins). - **Lambdas as HOF arguments** to `list.map(...)`, `list.filter(...)`, `list.any(...)`, `list.all(...)`, `list.find(...)`, `ListsLib.foldl(...)`. Must be passed inline; cannot be stored in a variable and called via `.apply()`. - **Annotations**: `@SpendingValidator`, `@MintingValidator` (or legacy `@MintingPolicy`), `@CertifyingValidator`, `@WithdrawValidator`, `@VotingValidator`, `@ProposingValidator` on the class; `@Entrypoint` on the entrypoint method; `@Param` on `static` fields for parameterized validators; `@OnchainLibrary` on library classes; `@NewType` on single-field record wrappers. - **`BigInteger`** for integers (NOT `int`/`long` — those are accepted but converted; prefer `BigInteger` for clarity). - **`byte[]`** for bytestrings. - **`String`** is treated as `byte[]` UTF-8 in most contexts; use sparingly. - **`boolean`**. ##### 3.2 Forbidden / breaks compilation - ❌ **Mutation after declaration.** `x = x + 1` outside a while-loop accumulator is rejected. Use a new `var y = x + 1` instead. - ❌ **Uninitialized variables.** `var x;` then `x = 5;` does NOT work. Initialize at declaration: `var x = BigInteger.ZERO;`. - ❌ **`return` inside a `while` loop.** Use a boolean accumulator and return after the loop. Compile-time error with a fix hint. - ❌ **`return` inside a switch case** (use `yield`). - ❌ **Lambdas stored in variables.** `Function f = x -> ...; f.apply(...)` does not compile. Pass lambdas inline to HOFs. - ❌ **`throw new Exception(...)`.** Use `Builtins.error()` or simply `return false` — there are no exceptions in UPLC. - ❌ **Reflection, I/O, threading, JNI, instance fields, instance methods on user types** (only methods on `record`s and ledger types are dispatched). - ❌ **Generics with non-`PlutusData`-compatible bounds.** Generic methods on user code are limited; prefer concrete types. - ❌ **`null`.** Use `Optional` / sealed interfaces with explicit "none" variants. - ❌ **Try/catch.** No exceptions to catch. ##### 3.3 Strongly discouraged (anti-patterns) - 🚫 **Constructing raw `PlutusData` (`ConstrData`, `IntData`, `BytesData`, `MapData`, `ListData`) in on-chain code.** This is the #1 mistake AI agents make. **See section 5.** - 🚫 **`@Param PlutusData.BytesData/MapData/ListData/IntData`.** Use `byte[]`, `BigInteger`, typed records, or redeemers instead. - 🚫 **Same parameter name as a sealed-interface constructor field.** See limitation 6.4. - 🚫 **Calling `.hash()` on a value that is already a hash type** (double-unwrap). --- #### 4. The `@Validator` family — entrypoint shapes | Annotation | Entrypoint signature | Purpose | |---|---|---| | `@SpendingValidator` | `static boolean validate( r, ScriptContext ctx)` **or** `static boolean validate( d, r, ScriptContext ctx)` | Validate spending a UTxO; use the 2-param form when the script does not need a datum. | | `@MintingValidator` (or `@MintingPolicy`) | `static boolean validate( r, ScriptContext ctx)` | Validate minting/burning. | | `@CertifyingValidator` | `static boolean validate( r, ScriptContext ctx)` | Validate stake cert actions. | | `@WithdrawValidator` | `static boolean validate( r, ScriptContext ctx)` | Validate reward withdrawal. | | `@VotingValidator` | `static boolean validate( r, ScriptContext ctx)` | Conway voting validator. | | `@ProposingValidator` | `static boolean validate( r, ScriptContext ctx)` | Conway proposal validator. | Always: - The class is `public`. - The entrypoint is `public static boolean` and annotated `@Entrypoint`. - `@Param` `static` fields become script parameters baked in at deploy time. #### 5. **CRITICAL: Use type classes, NOT raw PlutusData** This is the single most important rule. **An AI agent that ignores this rule produces verbose, brittle, non-idiomatic JuLC.** ##### 5.1 The rule In on-chain code, **always** prefer high-level ledger type classes: | ✅ Use this | ❌ NOT this | |---|---| | `TxOut`, `TxOutRef`, `TxInfo`, `ScriptContext`, `Address`, `Value`, `OutputDatum`, `Credential`, `IntervalBound`, `Interval` | `PlutusData.ConstrData(...)` for txouts/contexts | | `JulcList`, `JulcMap` | `PlutusData.ListData(...)`, `PlutusData.MapData(...)` | | `Optional` (with `.isPresent()` / `.get()` etc.) | `null`, raw `ConstrData` for None/Some | | `Tuple2`, `Tuple3` | Raw `ConstrData` 2-tuples | | Sealed interface with `record` variants (`switch` on it) | `ConstrData` with manual constructor-index checks | | `BigInteger`, `byte[]`, `boolean`, `String` | `PlutusData.IntData(...)`, `PlutusData.BytesData(...)` for values you own | The compiler infrastructure (TypeRegistrar, sealed-interface dispatch, named-record method dispatch, `JulcList`/`JulcMap` builtins) exists *precisely so you don't touch `PlutusData` directly*. ##### 5.2 When raw `PlutusData` is acceptable Only when interop genuinely requires it: - **Forwarding an opaque payload** from one contract to another where the payload type is not part of your contract. - **Generic redeemer slot** when the entrypoint accepts arbitrary data (e.g. `@Entrypoint validate(PlutusData redeemer, ScriptContext ctx)`). - **Datum-by-hash lookup** where the datum type is unknown at compile time. In all other cases, define a `record` and let the compiler handle encoding/decoding. ##### 5.3 Examples — bad vs good **❌ BAD: raw PlutusData** ```java @Entrypoint public static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { PlutusData.ConstrData d = (PlutusData.ConstrData) datum; PlutusData beneficiary = d.fields().get(0); byte[] beneficiaryBytes = ((PlutusData.BytesData) beneficiary).bytes(); // ... hand-decoding, error-prone, no compiler help } ``` **✅ GOOD: type-class style** ```java record VestingDatum(byte[] beneficiary) {} record VestingRedeemer(BigInteger amount) {} @Entrypoint public static boolean validate(VestingDatum datum, VestingRedeemer redeemer, ScriptContext ctx) { var sigs = ctx.txInfo().signatories(); return sigs.contains(PubKeyHash.of(datum.beneficiary())); } ``` The compiler emits the same UPLC for both, but the second form is type-safe, refactor-friendly, and idiomatic. --- #### 6. Known compiler limitations (READ EVERY ONE) These are real failure modes that have come up in JuLC's test suite. AI agents that ignore them produce code that compiles but behaves incorrectly, or fails to compile with cryptic errors. ##### 6.1 No `return` inside a `while` loop ```java // ❌ BAD — compile error while (i.compareTo(n) < 0) { if (matches(i)) return true; i = i.add(BigInteger.ONE); } return false; // ✅ GOOD — boolean accumulator boolean found = false; var i = BigInteger.ZERO; while (i.compareTo(n) < 0 && !found) { if (matches(i)) { found = true; } i = i.add(BigInteger.ONE); } return found; ``` `break` is allowed in `for-each`, but `return` inside `while` is rejected with an explicit error. ##### 6.2 Tuple2/Tuple3 are NOT switchable `Tuple2` is a `record` (a `RecordType`), not a sealed interface. You cannot `switch` on it. Use field access: ```java // ❌ BAD switch (pair) { case Tuple2(var a, var b) -> ... } // ✅ GOOD A a = pair.first(); B b = pair.second(); ``` ##### 6.3 `map()` returns `JulcList` `list.map(x -> ...)` always wraps the lambda's return in `Data` to keep the list element type uniform. So: ```java JulcList nums = ...; JulcList doubled = nums.map(n -> n.multiply(BigInteger.TWO)); // To use as integer: BigInteger first = Builtins.unIData(doubled.head()); ``` If you only need to *consume* the mapped list immediately (e.g. in a fold or any/all), use `any`/`all` directly with a predicate that includes the transformation. ##### 6.4 Switch case field name shadows method parameter If a sealed-interface variant has a field with the same name as a method parameter, the field binding **silently shadows** the parameter inside the switch case body. Always use distinct names. ```java // ❌ BAD — `time` parameter is shadowed by Finite's `time` field boolean check(IntervalBound bound, BigInteger time) { return switch (bound) { case Finite f -> f.time().compareTo(time) > 0; // `time` here = field, not param }; } // ✅ GOOD — rename param boolean check(IntervalBound bound, BigInteger point) { return switch (bound) { case Finite f -> f.time().compareTo(point) > 0; }; } ``` ##### 6.5 `@Param` type rules `@Param BigInteger` and `@Param byte[]` are supported and decoded by the parameter wrapper, so arithmetic and byte-string operations work normally. Do **not** use typed raw `PlutusData` subtypes as params: `PlutusData.BytesData`, `PlutusData.MapData`, `PlutusData.ListData`, and `PlutusData.IntData` are rejected with `JULC0013`. Use plain `PlutusData` only when the value must remain opaque. ##### 6.6 No double `.hash()` `PubKeyHash`, `ScriptHash`, etc. already store the hash bytes. Calling `.hash()` returns the bytes; calling `.hash().hash()` is wrong. The compiler usually catches this via the `hofUnwrappedVars` mechanism but be careful in HOF lambdas. ##### 6.7 `byte[]` casts off-chain in stdlib stubs `ByteStringLib.zeros()`, `ByteStringLib.empty()`, `ByteStringLib.integerToByteString()`, `ByteStringLib.serialiseData()` use `(byte[])(Object) Builtins.replicateByte(...)` etc. — these casts work on-chain but **fail off-chain at runtime** with `ClassCastException`. For off-chain testing, call `Builtins.*` methods directly, or test through UPLC evaluation. ##### 6.8 Building pair lists requires `MkNilPairData` When constructing a `Map`-style accumulator manually: ```java // ❌ BAD — element type mismatch at runtime var pairs = Builtins.mkNilData(); // Scalus VM rejects pair-list ops on this // ✅ GOOD var pairs = Builtins.mkNilPairData(); // Type the var as ListType(PairType(DataType, DataType)) ``` The compiler's `refineAccumulatorTypes()` auto-detects this in many cases, but if you build pair lists by hand, use `mkNilPairData`. ##### 6.9 BannedParamTypes `PlutusData.BytesData`, `PlutusData.MapData`, `PlutusData.ListData`, and `PlutusData.IntData` are **banned** from `@Param`. Compile-time error. ##### 6.10 No mutual recursion with >2 functions JuLC supports self-recursion and mutual recursion between exactly 2 helpers (via Bekic's theorem). Three or more mutually recursive functions are not supported. Refactor into a single function with an accumulator if needed. ##### 6.11 `String.equals(...)` works; `==` does not For `String` and `byte[]` comparison, use `.equals(...)` (compiles to `EqualsByteString`). `==` checks reference identity which is not meaningful in UPLC. ##### 6.12 `BigInteger` comparison: use `.compareTo(...)` `a == b` on `BigInteger` is reference equality. Use `a.equals(b)` or `a.compareTo(b) == 0`. For ordering, always `.compareTo(...)`. --- #### 7. Common errors — root cause and fix | Error message | Likely cause | Fix | |---|---|---| | `Method must have a body: ` | Abstract method in validator class | Provide a body for every method. | | `Variable must be initialized: ` | `var x;` without initializer | Initialize at declaration (`var x = BigInteger.ZERO;`). | | `Cannot return inside while loop` | `return` inside `while` body | Use a boolean accumulator; return after the loop. | | `Switch is not exhaustive on : missing ` | Sealed-interface switch missing cases | Add the missing case branches or a `default ->`. | | `Method does not return on all paths` | Missing return in some branch | Make every path return; void methods are exempt. | | `Type mismatch: expected IntegerType, got DataType` | Comparing `Builtins.unIData(...)` with raw `Data` field | Apply `unIData`/`unBData`/etc. to the right operand too. | | `Lambda cannot be stored in a variable` | Java `Function f = x -> ...; f.apply(...)` | Pass lambdas inline to HOFs; do not store. | | `Unknown method on ` | Stdlib method that does not exist | Check `ListsLib`, `ValuesLib`, etc.; check `JulcList`/`JulcMap` instance methods. | | `Banned param type: ` | `@Param PlutusData.BytesData/MapData/ListData/IntData` | Use `byte[]` for byte params, `BigInteger` for integer params, and typed records or redeemers for structured data. | | `Mutually recursive group too large` | Three+ mutually recursive helpers | Refactor; combine into one function with accumulators. | For the full troubleshooting guide, see [/reference/troubleshooting/](/reference/troubleshooting/). --- #### 8. Stdlib API surface (one-line signatures) All imports are from `com.bloxbean.cardano.julc.stdlib.lib.*`. ##### ContextsLib `signedBy(txInfo, pkh)`, `findOwnInput(ctx)`, `getContinuingOutputs(ctx)`, `findDatum(txInfo, hash)`, `valueSpent(txInfo)`, `valuePaid(txInfo, addr)`, `ownHash(ctx)`, `scriptOutputsAt(txInfo, hash)`, `listIndex(list, n)`, `trace(msg)`. Field shorthands: `txInfoInputs`, `txInfoOutputs`, `txInfoSignatories`, `txInfoValidRange`, `txInfoMint`, `txInfoFee`, `txInfoId`, `txInfoRefInputs`, `txInfoWithdrawals`, `txInfoRedeemers`. **Prefer `ctx.txInfo().outputs()` etc. via type-class field access.** ##### ListsLib `empty()`, `prepend(list, x)`, `length(list)`, `isEmpty(list)`, `head(list)`, `tail(list)`, `reverse(list)`, `concat(a, b)`, `nth(list, n)`, `take(list, n)`, `drop(list, n)`, `contains(list, x)`, `containsInt(list, n)`, `containsBytes(list, bs)`, `hasDuplicateInts(list)`, `hasDuplicateBytes(list)`, plus HOFs: `any(list, pred)`, `all(list, pred)`, `find(list, pred)`, `foldl(f, init, list)`, `map(list, f)`, `filter(list, pred)`, `zip(a, b)`. **Most of these are also instance methods on `JulcList`** — prefer `list.map(...)` over `ListsLib.map(list, ...)`. ##### ValuesLib `lovelaceOf(value)`, `assetOf(value, policy, token)`, `containsPolicy(value, policy)`, `geq(a, b)`, `geqMultiAsset(a, b)`, `leq(a, b)`, `eq(a, b)`, `isZero(value)`, `singleton(policy, token, amount)`, `negate(value)`, `flatten(value)`, `add(a, b)`, `subtract(a, b)`. **Prefer `value.lovelaceOf()`, `value.assetOf(policy, token)`, `value.isEmpty()` instance methods where available.** ##### MapLib `lookup(map, key)` (returns `Optional`), `member(map, key)`, `insert(map, k, v)`, `delete(map, k)`, `keys(map)`, `values(map)`, `toList(map)`, `fromList(list)`, `size(map)`. **Prefer instance methods on `JulcMap`**: `map.lookup(k)`, `map.get(k)`, `map.containsKey(k)`, `map.size()`, `map.isEmpty()`, `map.keys()`, `map.values()`, `map.insert(k, v)`, `map.delete(k)`. ##### OutputLib `txOutAddress(out)`, `txOutValue(out)`, `txOutDatum(out)`, `outputsAt(outputs, addr)`, `countOutputsAt(outputs, addr)`, `uniqueOutputAt(outputs, addr)`, `outputsWithToken(outputs, policy, token)`, `valueHasToken(value, policy, token)`, `lovelacePaidTo(outputs, addr)`, `paidAtLeast(outputs, addr, amount)`, `getInlineDatum(out)`, `resolveDatum(txInfo, out)`. **Prefer field access**: `out.address()`, `out.value()`, `out.datum()`. ##### MathLib `abs(n)`, `max(a, b)`, `min(a, b)`, `divMod(a, b)` → `Tuple2`, `quotRem(a, b)` → `Tuple2`, `pow(b, e)`, `sign(n)`, `expMod(b, e, m)`. ##### IntervalLib `between(lo, hi)`, `never()`, `isEmpty(interval)`, `finiteUpperBound(interval)`, `finiteLowerBound(interval)`, `contains(interval, point)`. ##### CryptoLib `verifyEcdsaSecp256k1(pk, msg, sig)`, `verifySchnorrSecp256k1(pk, msg, sig)`, `ripemd_160(bytes)`. Plus the SHA-2/SHA-3/Blake2b family via `Builtins.*`. ##### ByteStringLib `take(bs, n)`, `lessThan(a, b)`, `lessThanEquals(a, b)`, `integerToByteString(big, len, value)`, `byteStringToInteger(big, bs)`, `encodeUtf8(s)`, `decodeUtf8(bs)`, `serialiseData(d)`. ⚠ Off-chain casts may throw — see limitation 6.7. ##### BitwiseLib `andByteString(pad, a, b)`, `orByteString(...)`, `xorByteString(...)`, `complementByteString(bs)`, `readBit(bs, i)`, `writeBits(bs, ...)`, `shiftByteString(bs, n)`, `rotateByteString(bs, n)`, `countSetBits(bs)`, `findFirstSetBit(bs)`. ##### AddressLib `credentialHash(addr)` → `byte[]`, `isScriptAddress(addr)`, `isPubKeyAddress(addr)`, `paymentCredential(addr)` → `Credential`. ##### BlsLib (Plutus V3) G1/G2 add/scale/neg, pairing, MSM, and `bls12_381_finalVerify`. ##### NativeValueLib (PV11) Native Mary-era `Value` operations. ##### Builtins (`com.bloxbean.cardano.julc.stdlib.Builtins`) Plutus builtins exposed by the Java API include `equalsByteString`, `equalsData`, `unBData`, `unIData`, `unMapData`, `unListData`, `unConstrData`, `iData`, `bData`, `mapData`, `listData`, `constrData`, `mkCons`, `mkNilData`, `mkNilPairData`, `nullList`, `headList`, `tailList`, `fstPair`, `sndPair`, `constrTag`, `constrFields`, `error`, `trace`, `replicateByte`, `serialiseData`, byte-string helpers such as `appendByteString`, `sliceByteString`, `integerToByteString`, `byteStringToInteger`, hashing (`sha2_256`, `sha3_256`, `blake2b_256`, `blake2b_224`, `keccak_256`, `ripemd_160`), and BLS helpers. Use Java operators / `BigInteger` methods for integer arithmetic; there is no public `Builtins.addInteger(...)` API. ##### Full machine-readable stdlib reference *Auto-generated from `julc-stdlib/.../lib/*.java`. Source of truth: [/ai/catalog.json](/ai/catalog.json).* ###### AddressLib *Address operations compiled from Java source to UPLC.* - `byte[] credentialHash(Address address)` — Extract the hash from the payment credential of an address. Works for both PubKeyCredential and ScriptCredential. Uses chained field access: Credential variant -> hash wrapper -> byte[]. - `boolean isScriptAddress(Address address)` — Check if an address has a ScriptCredential (tag == 1). - `boolean isPubKeyAddress(Address address)` — Check if an address has a PubKeyCredential (tag == 0). - `Credential paymentCredential(Address address)` — Extract the payment credential from an address. ###### BitwiseLib *Bitwise operations on ByteStrings compiled from Java source to UPLC.* - `byte[] andByteString(boolean padding, byte[] a, byte[] b)` — Bitwise AND of two bytestrings with padding semantics. - `byte[] orByteString(boolean padding, byte[] a, byte[] b)` — Bitwise OR of two bytestrings with padding semantics. - `byte[] xorByteString(boolean padding, byte[] a, byte[] b)` — Bitwise XOR of two bytestrings with padding semantics. - `byte[] complementByteString(byte[] bs)` — Bitwise complement of a bytestring. - `boolean readBit(byte[] bs, long index)` — Read a bit at a given index. - `byte[] shiftByteString(byte[] bs, long n)` — Shift a bytestring by n bits. - `byte[] rotateByteString(byte[] bs, long n)` — Rotate a bytestring by n bits. - `long countSetBits(byte[] bs)` — Count the number of set bits (1s) in a bytestring. - `long findFirstSetBit(byte[] bs)` — Find the index of the first set bit, or -1 if none. ###### BlsLib *BLS12-381 elliptic curve operations compiled from Java source to UPLC.* - `byte[] g1Add(byte[] a, byte[] b)` — Add two G1 elements. - `byte[] g1Neg(byte[] a)` — Negate a G1 element. - `byte[] g1ScalarMul(BigInteger scalar, byte[] g1)` — Scalar multiplication of a G1 element. - `boolean g1Equal(byte[] a, byte[] b)` — Check equality of two G1 elements. - `byte[] g1Compress(byte[] g1)` — Compress a G1 element to 48 bytes. - `byte[] g1Uncompress(byte[] compressed)` — Uncompress a 48-byte compressed G1 element. - `byte[] g1HashToGroup(byte[] msg, byte[] dst)` — Hash a message to a G1 element using the given domain separation tag. - `byte[] g2Add(byte[] a, byte[] b)` — Add two G2 elements. - `byte[] g2Neg(byte[] a)` — Negate a G2 element. - `byte[] g2ScalarMul(BigInteger scalar, byte[] g2)` — Scalar multiplication of a G2 element. - `boolean g2Equal(byte[] a, byte[] b)` — Check equality of two G2 elements. - `byte[] g2Compress(byte[] g2)` — Compress a G2 element to 96 bytes. - `byte[] g2Uncompress(byte[] compressed)` — Uncompress a 96-byte compressed G2 element. - `byte[] g2HashToGroup(byte[] msg, byte[] dst)` — Hash a message to a G2 element using the given domain separation tag. - `byte[] millerLoop(byte[] g1, byte[] g2)` — Compute the Miller loop pairing of a G1 and G2 element. - `byte[] mulMlResult(byte[] a, byte[] b)` — Multiply two Miller loop results. - `boolean finalVerify(byte[] a, byte[] b)` — Final verification of two Miller loop results. Returns true if the pairing check passes. - `byte[] g1MultiScalarMul(PlutusData scalars, PlutusData points)` — Multi-scalar multiplication on G1. Takes a list of scalars and a list of G1 elements. - `byte[] g2MultiScalarMul(PlutusData scalars, PlutusData points)` — Multi-scalar multiplication on G2. Takes a list of scalars and a list of G2 elements. ###### ByteStringLib *ByteString operations compiled from Java source to UPLC.* - `long at(byte[] bs, long index)` — Get the byte at a given index (0-255). - `byte[] cons(long byte_, byte[] bs)` — Prepend a byte (0-255) to a bytestring. - `byte[] slice(byte[] bs, long start, long length)` — Extract a slice: slice(bs, start, length). - `long length(byte[] bs)` — Get the length of a bytestring. - `byte[] drop(byte[] bs, long n)` — Drop the first n bytes from a bytestring. - `byte[] take(byte[] bs, long n)` — Take the first n bytes from a bytestring. - `byte[] append(byte[] a, byte[] b)` — Concatenate two bytestrings. - `byte[] empty()` — An empty bytestring. - `byte[] zeros(long n)` — Create a bytestring of n zero bytes. - `boolean equals(byte[] a, byte[] b)` — Compare two bytestrings for equality. - `boolean lessThan(byte[] a, byte[] b)` — Lexicographic comparison: a < b. - `boolean lessThanEquals(byte[] a, byte[] b)` — Lexicographic comparison: a <= b. - `byte[] integerToByteString(boolean endian, long width, long i)` — Convert integer to bytestring. - `byte[] integerToByteString(boolean endian, long width, BigInteger i)` — Convert integer to bytestring. Accepts arbitrary-precision BigInteger. - `BigInteger byteStringToInteger(boolean endian, byte[] bs)` — Convert bytestring to integer. Returns arbitrary-precision BigInteger. - `byte[] encodeUtf8(PlutusData s)` — Encode a string as UTF-8 bytestring. - `PlutusData decodeUtf8(byte[] bs)` — Decode a UTF-8 bytestring. - `byte[] serialiseData(PlutusData d)` — Serialise a Data value to CBOR bytes. - `long hexNibble(long n)` — Map a nibble (0-15) to its lowercase ASCII hex char ('0'-'9', 'a'-'f'). - `byte[] toHex(byte[] bs)` — Convert a bytestring to its lowercase hex representation. Each byte becomes two hex characters (e.g. 0xDE → "de"). - `byte[] toHexStep(byte[] bs, long idx, byte[] acc)` — Recursive helper: process bytes from index down to 0, prepending hex chars. - `byte[] intToDecimalString(BigInteger n)` — Convert a non-negative integer to its decimal string representation as UTF-8 bytes. E.g. 42 → "42" (as byte[]{52, 50}). - `byte[] intToDecimalStep(BigInteger n, byte[] acc)` — Recursive helper: divmod by 10, prepend digit char. - `BigInteger utf8ToInteger(byte[] bs)` — Parse a UTF-8 decimal string (e.g. bytes of "42") to an integer. Inverse of #intToDecimalString(BigInteger). Assumes all bytes are ASCII digits ('0'-'9'). No sign handling. - `BigInteger utf8ToIntegerStep(byte[] bs, long idx, long len, BigInteger acc)` — Recursive helper: accumulate decimal value left-to-right. ###### ContextsLib *Script context operations compiled from Java source to UPLC.* - `void trace(String message)` — Emits a trace message. On-chain becomes UPLC Trace builtin. Body is a no-op — the compiler uses the PIR-registered version. - `TxInfo getTxInfo(ScriptContext ctx)` — Extracts the TxInfo from a ScriptContext. - `PlutusData getRedeemer(ScriptContext ctx)` — Extracts the redeemer from a ScriptContext. - `JulcList txInfoInputs(TxInfo txInfo)` — Extracts the list of inputs from a TxInfo. - `JulcList txInfoOutputs(TxInfo txInfo)` — Extracts the list of outputs from a TxInfo. - `JulcList txInfoSignatories(TxInfo txInfo)` — Extracts the signatories list from a TxInfo. - `Interval txInfoValidRange(TxInfo txInfo)` — Extracts the valid range from a TxInfo. - `Value txInfoMint(TxInfo txInfo)` — Extracts the mint field from a TxInfo. - `BigInteger txInfoFee(TxInfo txInfo)` — Extracts the fee from a TxInfo. - `TxId txInfoId(TxInfo txInfo)` — Extracts the txId from a TxInfo. - `JulcList txInfoRefInputs(TxInfo txInfo)` — Extracts reference inputs from TxInfo. - `JulcMap txInfoWithdrawals(TxInfo txInfo)` — Extracts withdrawals map from TxInfo. - `JulcMap txInfoRedeemers(TxInfo txInfo)` — Extracts redeemers map from TxInfo. - `boolean signedBy(TxInfo txInfo, byte[] pkh)` — Checks whether a given PubKeyHash is in the signatories list. - `Optional getSpendingDatum(ScriptContext ctx)` — Extracts the optional datum from a spending ScriptContext. Returns Optional.of(datum) for SpendingScript with datum, Optional.empty() otherwise. - `Optional findOwnInput(ScriptContext ctx)` — Finds the own input for a spending validator. Returns Optional. - `byte[] ownInputScriptHash(ScriptContext ctx)` — Extracts the script credential hash from the own input's address. - `byte[] ownHash(ScriptContext ctx)` — Extracts the own script hash from the ScriptContext's ScriptInfo. Minting -> policyId. Others -> script credential hash from own input. - `JulcList getContinuingOutputs(ScriptContext ctx)` — Returns outputs that pay to the same address as the own spending input. - `JulcList valueSpent(TxInfo txInfo)` — Collects the values of all inputs as a list. - `JulcList valuePaid(TxInfo txInfo, Address addr)` — Filters outputs by address and returns their values as a list. - `JulcList scriptOutputsAt(TxInfo txInfo, byte[] scriptHash)` — Filters outputs whose address has a ScriptCredential matching the given hash. - `Optional findDatum(TxInfo txInfo, PlutusData hash)` — Searches the txInfo datums map for a datum matching the given hash. Returns Optional. ###### CryptoLib *Cryptographic hash and signature verification operations compiled from Java source to UPLC.* - `byte[] sha2_256(byte[] bs)` — SHA2-256 hash. - `byte[] blake2b_256(byte[] bs)` — Blake2b-256 hash. - `boolean verifyEd25519Signature(byte[] key, byte[] msg, byte[] sig)` — Verify an Ed25519 signature. - `byte[] sha3_256(byte[] bs)` — SHA3-256 hash. - `byte[] blake2b_224(byte[] bs)` — Blake2b-224 hash (commonly used for key hashes). - `byte[] keccak_256(byte[] bs)` — Keccak-256 hash. - `boolean verifyEcdsaSecp256k1(byte[] vk, byte[] msg, byte[] sig)` — Verify ECDSA secp256k1 signature. - `boolean verifySchnorrSecp256k1(byte[] vk, byte[] msg, byte[] sig)` — Verify Schnorr secp256k1 signature. - `byte[] ripemd_160(byte[] bs)` — RIPEMD-160 hash. ###### IntervalLib *Interval / POSIXTimeRange operations compiled from Java source to UPLC.* - `boolean contains(Interval interval, BigInteger point)` — Checks whether a point in time is contained within an interval. - `Interval always()` — Builds the "always" interval: (-inf, +inf). - `Interval after(BigInteger t)` — Builds the interval [t, +inf). - `Interval before(BigInteger t)` — Builds the interval (-inf, t]. - `Interval between(BigInteger low, BigInteger high)` — Builds the interval [low, high] (both inclusive). - `Interval never()` — Builds the empty interval (PosInf, NegInf). - `boolean isEmpty(Interval interval)` — Checks if an interval is empty (lower is PosInf or upper is NegInf). - `BigInteger finiteUpperBound(Interval interval)` — Extract the finite upper bound time, or return -1 if not finite. - `BigInteger finiteLowerBound(Interval interval)` — Extract the finite lower bound time, or return -1 if not finite. ###### ListsLib *List operations compiled from Java source to UPLC.* - `JulcList empty()` — Return an empty list. - `JulcList prepend(JulcList list, PlutusData element)` — Prepend an element to the front of a list. - `long length(JulcList list)` — Return the number of elements in the list. - `boolean isEmpty(JulcList list)` — Return true if the list is empty. - `PlutusData head(JulcList list)` — Return the first element of the list. - `JulcList tail(JulcList list)` — Return all elements except the first. - `JulcList reverse(JulcList list)` — Reverse a list. - `JulcList concat(JulcList a, JulcList b)` — Concatenate two lists. - `PlutusData nth(JulcList list, long n)` — Get element at index n (0-based). - `JulcList take(JulcList list, long n)` — Take the first n elements from a list. - `JulcList drop(JulcList list, long n)` — Drop the first n elements from a list. - `boolean contains(JulcList list, PlutusData target)` — Return true if list contains target (using EqualsData). - `boolean containsInt(JulcList list, BigInteger target)` — Check if a list of integers contains the given value. Uses EqualsInteger. - `boolean hasDuplicateInts(JulcList list)` — Check if a list of integers contains any duplicate. O(n^2). Delegates to #hasDuplicates. - `boolean hasDuplicateBytes(JulcList list)` — Check if a list of bytestrings contains any duplicate. O(n^2). Delegates to #hasDuplicates. - `boolean hasDuplicates(JulcList list)` — Check if a list contains any duplicate element. O(n^2). For each element, searches the tail using EqualsData. - `boolean containsBytes(JulcList list, byte[] target)` — Check if a list of bytestrings contains the given target. Uses EqualsByteString. ###### MapLib *Map (association list) operations compiled from Java source to UPLC.* - `JulcMap empty()` — Return an empty map. - `Optional lookup(JulcMap map, PlutusData key)` — Look up a key. Returns Optional.of(value) if found, Optional.empty() if not. - `boolean member(JulcMap map, PlutusData key)` — Check if key exists in map. - `JulcMap insert(JulcMap map, PlutusData key, PlutusData value)` — Insert key-value pair (prepends; shadows existing). - `JulcMap delete(JulcMap map, PlutusData key)` — Delete a key from the map. - `JulcList keys(JulcMap map)` — Extract all keys from a map as a list. - `JulcList values(JulcMap map)` — Extract all values from a map as a list. - `JulcMap toList(JulcMap map)` — Convert map to pair list (identity — MapType vars already hold pair lists). - `JulcMap fromList(PlutusData list)` — Construct map from pair list (MapData). - `long size(JulcMap map)` — Count entries in the map. ###### MathLib *Mathematical operations compiled from Java source to UPLC.* - `BigInteger abs(BigInteger x)` — Returns the absolute value of an integer. - `BigInteger max(BigInteger a, BigInteger b)` — Returns the maximum of two integers. - `BigInteger min(BigInteger a, BigInteger b)` — Returns the minimum of two integers. - `Tuple2 divMod(BigInteger a, BigInteger b)` — Returns division and modulo as a Tuple2. - `Tuple2 quotRem(BigInteger a, BigInteger b)` — Returns quotient and remainder as a Tuple2. - `BigInteger pow(BigInteger base, BigInteger exp)` — Returns base raised to the power of exp. - `BigInteger expMod(BigInteger base, BigInteger exp, BigInteger mod)` — Returns (base^exp) mod modulus using the builtin ExpModInteger operation. - `BigInteger sign(BigInteger x)` — Returns -1 if negative, 0 if zero, 1 if positive. ###### NativeValueLib *Native MaryEra Value operations using PV11 builtins (CIP-153).* - `PlutusData fromData(PlutusData mapData)` — Convert Map-encoded PlutusData to native Value. - `PlutusData toData(PlutusData value)` — Convert native Value back to Map-encoded PlutusData. - `PlutusData insertCoin(byte[] policyId, byte[] tokenName, BigInteger amount, PlutusData value)` — Insert or update a token quantity in a Value. - `BigInteger lookupCoin(byte[] policyId, byte[] tokenName, PlutusData value)` — Look up a token quantity. Returns 0 if absent. - `PlutusData union(PlutusData a, PlutusData b)` — Merge two Values by adding quantities. - `boolean contains(PlutusData a, PlutusData b)` — Check if Value a contains at least Value b (a >= b element-wise). - `PlutusData scale(BigInteger scalar, PlutusData value)` — Scale all quantities by a scalar. ###### OutputLib *Transaction output utility operations compiled from Java source to UPLC.* - `Address txOutAddress(TxOut txOut)` — Extract the address from a TxOut. - `Value txOutValue(TxOut txOut)` — Extract the value from a TxOut. - `OutputDatum txOutDatum(TxOut txOut)` — Extract the datum from a TxOut. - `JulcList outputsAt(JulcList outputs, Address address)` — Return all outputs sent to the given address. - `long countOutputsAt(JulcList outputs, Address address)` — Count the number of outputs sent to the given address. - `TxOut uniqueOutputAt(JulcList outputs, Address address)` — Return the unique output at the given address. Aborts if not exactly one match. - `JulcList outputsWithToken(JulcList outputs, byte[] policyId, byte[] tokenName)` — Return all outputs containing the specified token (amount > 0). - `boolean valueHasToken(Value value, byte[] policyId, byte[] tokenName)` — Check if a value contains any amount of the specified token. - `BigInteger lovelacePaidTo(JulcList outputs, Address address)` — Sum the lovelace in all outputs sent to the given address. - `boolean paidAtLeast(JulcList outputs, Address address, BigInteger minLovelace)` — Check if the total lovelace paid to the address meets the minimum threshold. - `PlutusData getInlineDatum(TxOut txOut)` — Extract the inline datum from a TxOut. Aborts if the datum is not inline. - `PlutusData resolveDatum(TxOut txOut, JulcMap datumsMap)` — Resolve the datum from a TxOut: inline datum is returned directly, datum hash is looked up in the datums map, NoDatum aborts. - `TxOut findOutputWithToken(JulcList outputs, byte[] scriptHash, byte[] policyId, byte[] tokenName)` — Find the first output at a script address (identified by scriptHash) containing the specified token with inline datum. Aborts if not found. - `TxInInfo findInputWithToken(JulcList inputs, byte[] scriptHash, byte[] policyId, byte[] tokenName)` — Find the first input at a script address containing the specified token with inline datum. Aborts if not found. ###### ValuesLib *Value manipulation operations compiled from Java source to UPLC.* - `BigInteger lovelaceOf(Value value)` — Extracts the lovelace (ADA) amount from a Value. - `boolean containsPolicy(Value value, byte[] policyId)` — Check if a policy ID (as bytes) exists in a Value. - `boolean geq(Value a, Value b)` — Checks if lovelaceOf(a) >= lovelaceOf(b). - `BigInteger assetOf(Value value, byte[] policyId, byte[] tokenName)` — Extracts the amount of a specific asset. Returns 0 if not found. - `boolean geqMultiAsset(Value a, Value b)` — Checks if value a >= value b for ALL policy/token pairs (multi-asset). - `boolean leq(Value a, Value b)` — Checks if value a <= value b (multi-asset). - `boolean eq(Value a, Value b)` — Checks if two values are equal (multi-asset). - `boolean isZero(Value value)` — Checks if a value is zero (all amounts == 0). - `Value singleton(byte[] policyId, byte[] tokenName, BigInteger amount)` — Constructs a Value containing a single asset: Map[(policy, Map[(token, amount)])]. - `Value negate(Value value)` — Negates all amounts in a value. - `JulcList flatten(Value value)` — Flattens a Value into a list of (policy, token, amount) triples as ConstrData(0, [p, t, amt]). - `JulcList flattenTyped(Value value)` — Typed variant of #flatten(Value) that returns `JulcList`. Each element provides typed field access: `entry.policyId()`, `entry.tokenName()`, `entry.amount()`. - `Value add(Value a, Value b)` — Adds two Values together (union, adding amounts for matching policy/token). - `Value subtract(Value a, Value b)` — Subtracts value b from value a: add(a, negate(b)). - `BigInteger countTokensWithQty(Value mint, byte[] policyId, BigInteger expectedQty)` — Count tokens with a specific quantity under a specific policy in a Value/mint. E.g., count how many tokens were minted with qty=1 under a given policy. - `byte[] findTokenName(Value mint, byte[] policyId, BigInteger expectedQty)` — Find the first token name with a specific quantity under a specific policy. Returns empty bytestring if not found. --- #### 9. Ledger types (always prefer over raw PlutusData) All ledger types live in `com.bloxbean.cardano.julc.ledger.*`. ##### Core context - **`ScriptContext`** — `txInfo() : TxInfo`, `redeemer() : PlutusData`, `scriptInfo() : ScriptInfo`. - **`TxInfo`** — `inputs() : JulcList`, `referenceInputs()`, `outputs() : JulcList`, `fee() : BigInteger`, `mint() : Value`, `certificates() : JulcList`, `withdrawals() : JulcMap`, `validRange() : Interval`, `signatories() : JulcList`, `redeemers() : JulcMap`, `datums() : JulcMap`, `id() : TxId`, `votes()`, `proposalProcedures()`. - **`TxInInfo`** — `outRef() : TxOutRef`, `resolved() : TxOut`. - **`TxOut`** — `address() : Address`, `value() : Value`, `datum() : OutputDatum`, `referenceScript() : Optional`. - **`TxOutRef`** — `txId() : TxId`, `index() : BigInteger`. - **`ScriptInfo`** sealed interface: `MintingScript(policyId)`, `SpendingScript(txOutRef, datum)`, `RewardingScript(credential)`, `CertifyingScript(index, cert)`, `VotingScript(voter)`, `ProposingScript(index, procedure)`. ##### Address & credentials - **`Address`** — `credential() : Credential`, `stakingCredential() : Optional`. - **`Credential`** sealed: `PubKeyCredential(hash)`, `ScriptCredential(hash)`. Switch on it for address-type checks. - **`StakingCredential`** sealed: `StakingHash(credential)`, `StakingPtr(slot, txIndex, certIndex)`. ##### Hashes (newtype wrappers around `byte[]`) - `PubKeyHash`, `ScriptHash`, `ValidatorHash`, `PolicyId`, `TokenName`, `DatumHash`, `TxId` — each has `.hash() : byte[]` and a `.of(byte[])` factory: `PubKeyHash.of(bytes)`. **Never use `(PubKeyHash)(Object) bytes` casts** — use `.of(...)`. ##### Value & assets - **`Value`** — Multi-asset value. Methods: `lovelaceOf() : BigInteger`, `assetOf(policy, token) : BigInteger`, `isEmpty() : boolean`. For arithmetic use `ValuesLib.add/subtract/eq/leq/geq`. ##### Datum - **`OutputDatum`** sealed: `NoOutputDatum()`, `OutputDatumHash(hash)`, `OutputDatumInline(datum)`. Switch on it to extract. ##### Time - **`Interval`** — `from() : IntervalBound`, `to() : IntervalBound`. - **`IntervalBound`** — `boundType() : IntervalBoundType`, `isInclusive() : boolean`. - **`IntervalBoundType`** sealed: `NegInf()`, `Finite(time)`, `PosInf()`. Watch for **limitation 6.4** if you switch on this. ##### Conway governance - **`Vote`** sealed: `Yes()`, `No()`, `Abstain()`. - **`DRep`** sealed: `DRepCredential(credential)`, `AlwaysAbstain()`, `AlwaysNoConfidence()`. - **`Voter`** sealed: `CommitteeVoter(credential)`, `DRepVoter(credential)`, `StakePoolVoter(pkh)`. - **`GovernanceAction`** sealed (many variants): `ParameterChange`, `HardForkInitiation`, `TreasuryWithdrawals`, `NoConfidence`, `UpdateCommittee`, `NewConstitution`, `InfoAction`. - **`TxCert`** sealed: many cert variants. - **`ProposalProcedure`** record. - **`ScriptPurpose`** sealed: `MintingPurpose`, `SpendingPurpose`, `RewardingPurpose`, `CertifyingPurpose`, `VotingPurpose`, `ProposingPurpose`. ##### Generic - **`Optional`** — `Optional.of(x)` / `Optional.empty()` factories, `.isPresent()`, `.get()`, `.orElse(x)`. Compiled to `ConstrData(0, [x])` / `ConstrData(1, [])`. - **`Tuple2`** — `.first()`, `.second()`. Use field access; **not switchable** (limitation 6.2). - **`Tuple3`** — `.first()`, `.second()`, `.third()`. - **`JulcList`** — On-chain list. Instance methods: `head()`, `tail()`, `get(i)`, `size()`, `isEmpty()`, `contains(x)`, `prepend(x)`, `reverse()`, `concat(other)`, `take(n)`, `drop(n)`, `map(f)`, `filter(p)`, `any(p)`, `all(p)`, `find(p)`. **Iterable**: `for (var x : list) { ... }`. - **`JulcMap`** — On-chain map (association list of pairs). Instance methods: `get(k)`, `lookup(k) : Optional`, `containsKey(k)`, `size()`, `isEmpty()`, `keys()`, `values()`, `insert(k, v)`, `delete(k)`. **Iterable as pairs**: `for (var entry : map) { K k = entry.key(); V v = entry.value(); }`. ##### Full machine-readable ledger types reference *Auto-generated from `julc-ledger-api/.../ledger/*.java`. Source of truth: [/ai/catalog.json](/ai/catalog.json).* ###### Records - **`Address`** — A Cardano address: a payment credential and optional staking credential. - Fields: `Credential credential`, `Optional stakingCredential` - **`Committee`** — A governance committee. - Fields: `JulcMap members`, `Rational quorum` - **`DatumHash`** — A datum hash (typically 32 bytes). No byte-length validation — length enforcement is a ledger rule, not a type invariant. - Fields: `byte[] hash` - Methods: `byte[] hash()`, `DatumHash of(byte[] hash)` - **`GovernanceActionId`** — A governance action identifier. - Fields: `TxId txId`, `BigInteger govActionIx` - **`Interval`** — A time interval with lower and upper bounds. - Fields: `IntervalBound from`, `IntervalBound to` - Methods: `Interval always()`, `Interval never()`, `Interval after(BigInteger time)`, `Interval before(BigInteger time)`, `Interval between(BigInteger from, BigInteger to)` - **`IntervalBound`** — A bound of an interval with inclusivity flag. - Fields: `IntervalBoundType boundType`, `boolean isInclusive` - **`PolicyId`** — A minting policy ID (typically 28 bytes, or 0 bytes for ADA). No byte-length validation — length enforcement is a ledger rule, not a type invariant. - Fields: `byte[] hash` - Methods: `byte[] hash()`, `PolicyId of(byte[] hash)` - **`ProposalProcedure`** — A governance proposal procedure. - Fields: `BigInteger deposit`, `Credential returnAddress`, `GovernanceAction governanceAction` - **`ProtocolVersion`** — A protocol version (major, minor). - Fields: `BigInteger major`, `BigInteger minor` - **`PubKeyHash`** — A public key hash (typically 28 bytes). No byte-length validation — length enforcement is a ledger rule, not a type invariant. - Fields: `byte[] hash` - Methods: `byte[] hash()`, `PubKeyHash of(byte[] hash)` - **`Rational`** — A rational number (numerator / denominator). - Fields: `BigInteger numerator`, `BigInteger denominator` - **`ScriptContext`** — V3 script context: transaction info + redeemer + script info. - Fields: `TxInfo txInfo`, `PlutusData redeemer`, `ScriptInfo scriptInfo` - **`ScriptHash`** — A script hash (typically 28 bytes). No byte-length validation — length enforcement is a ledger rule, not a type invariant. - Fields: `byte[] hash` - Methods: `byte[] hash()`, `ScriptHash of(byte[] hash)` - **`TokenName`** — A token name (typically 0-32 bytes). No byte-length validation — length enforcement is a ledger rule, not a type invariant. - Fields: `byte[] name` - Methods: `byte[] name()`, `TokenName of(byte[] name)` - **`TxId`** — A transaction ID (typically 32 bytes). No byte-length validation — length enforcement is a ledger rule, not a type invariant. - Fields: `byte[] hash` - Methods: `byte[] hash()`, `TxId of(byte[] hash)` - **`TxInInfo`** — An input being consumed or referenced by a transaction. - Fields: `TxOutRef outRef`, `TxOut resolved` - **`TxInfo`** — V3 (Conway) transaction info with 16 fields. - Fields: `JulcList inputs`, `JulcList referenceInputs`, `JulcList outputs`, `BigInteger fee`, `Value mint`, `JulcList certificates`, `JulcMap withdrawals`, `Interval validRange`, `JulcList signatories`, `JulcMap redeemers`, `JulcMap datums`, `TxId id`, `JulcMap> votes`, `JulcList proposalProcedures`, `Optional currentTreasuryAmount`, `Optional treasuryDonation` - **`TxOut`** — A transaction output. - Fields: `Address address`, `Value value`, `OutputDatum datum`, `Optional referenceScript` - **`TxOutRef`** — A reference to a transaction output (TxId + output index). - Fields: `TxId txId`, `BigInteger index` - **`ValidatorHash`** — A validator hash (typically 28 bytes, semantically an alias for ScriptHash). No byte-length validation — length enforcement is a ledger rule, not a type invariant. - Fields: `byte[] hash` - Methods: `byte[] hash()`, `ValidatorHash of(byte[] hash)` - **`Value`** — A multi-asset value: JulcMap<PolicyId, JulcMap<TokenName, BigInteger>>. - Fields: `JulcMap> inner` - Methods: `Value zero()`, `Value lovelace(BigInteger amount)`, `Value singleton(PolicyId policyId, TokenName tokenName, BigInteger quantity)`, `BigInteger lovelaceOf()`, `boolean containsPolicy(PolicyId policyId)`, `BigInteger assetOf(PolicyId policyId, TokenName tokenName)`, `boolean isEmpty()`, `Value merge(Value other)` ###### Sealed interfaces - **`Credential`** — A credential: either a public key hash or a script hash. - `PubKeyCredential(PubKeyHash hash)` - `ScriptCredential(ScriptHash hash)` - **`DRep`** — A delegated representative (DRep). - `DRepCredential(Credential credential)` - `AlwaysAbstain()` - `AlwaysNoConfidence()` - **`Delegatee`** — A delegation target. - `Stake(PubKeyHash poolId)` - `Vote(DRep dRep)` - `StakeVote(PubKeyHash poolId, DRep dRep)` - **`GovernanceAction`** — A governance action (7 variants). - `ParameterChange(Optional id, PlutusData parameters, Optional constitutionScript)` - `HardForkInitiation(Optional id, ProtocolVersion protocolVersion)` - `TreasuryWithdrawals(JulcMap withdrawals, Optional constitutionScript)` - `NoConfidence(Optional id)` - `UpdateCommittee(Optional id, JulcList removedMembers, JulcMap addedMembers, Rational newQuorum)` - `NewConstitution(Optional id, Optional constitution)` - `InfoAction()` - **`IntervalBoundType`** — The type of an interval bound: negative infinity, finite, or positive infinity. - `NegInf()` - `Finite(BigInteger time)` - `PosInf()` - **`OutputDatum`** — Datum attached to a transaction output. - `NoOutputDatum()` - `OutputDatumHash(DatumHash hash)` - `OutputDatumInline(PlutusData datum)` - **`ScriptInfo`** — Information about the currently executing script (6 variants). - `MintingScript(PolicyId policyId)` - `SpendingScript(TxOutRef txOutRef, Optional datum)` - `RewardingScript(Credential credential)` - `CertifyingScript(BigInteger index, TxCert cert)` - `VotingScript(Voter voter)` - `ProposingScript(BigInteger index, ProposalProcedure procedure)` - **`ScriptPurpose`** — The purpose of a script execution (6 variants). - `Minting(PolicyId policyId)` - `Spending(TxOutRef txOutRef)` - `Rewarding(Credential credential)` - `Certifying(BigInteger index, TxCert cert)` - `Voting(Voter voter)` - `Proposing(BigInteger index, ProposalProcedure procedure)` - **`StakingCredential`** — A staking credential: either a hash-based credential or a pointer. - `StakingHash(Credential credential)` - `StakingPtr(BigInteger slot, BigInteger txIndex, BigInteger certIndex)` - **`TxCert`** — A transaction certificate (V3 Conway era, 11 variants). - `RegStaking(Credential credential, Optional deposit)` - `UnRegStaking(Credential credential, Optional refund)` - `DelegStaking(Credential credential, Delegatee delegatee)` - `RegDeleg(Credential credential, Delegatee delegatee, BigInteger deposit)` - `RegDRep(Credential credential, BigInteger deposit)` - `UpdateDRep(Credential credential)` - `UnRegDRep(Credential credential, BigInteger refund)` - `PoolRegister(PubKeyHash poolId, PubKeyHash poolVfr)` - `PoolRetire(PubKeyHash pubKeyHash, BigInteger epoch)` - `AuthHotCommittee(Credential cold, Credential hot)` - `ResignColdCommittee(Credential cold)` - **`Vote`** — A governance vote. - `VoteNo()` - `VoteYes()` - `Abstain()` - **`Voter`** — A governance voter. - `CommitteeVoter(Credential credential)` - `DRepVoter(Credential credential)` - `StakePoolVoter(PubKeyHash pubKeyHash)` --- #### 10. Canonical examples (idiomatic JuLC) These are real, tested validators from [`julc-examples`](https://github.com/bloxbean/julc-examples). **Use these as templates.** ##### 10.1 Spending validator with typed datum/redeemer (vesting) ```java package com.example.validators; import com.bloxbean.cardano.julc.stdlib.annotation.SpendingValidator; import com.bloxbean.cardano.julc.stdlib.annotation.Entrypoint; import com.bloxbean.cardano.julc.ledger.ScriptContext; import com.bloxbean.cardano.julc.ledger.TxInfo; import com.bloxbean.cardano.julc.ledger.PubKeyHash; import java.math.BigInteger; @SpendingValidator public class VestingValidator { record VestingDatum(byte[] beneficiary, BigInteger deadline) {} record VestingRedeemer(BigInteger action) {} @Entrypoint public static boolean validate(VestingDatum datum, VestingRedeemer redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); return txInfo.signatories().contains(PubKeyHash.of(datum.beneficiary())); } } ``` ##### 10.2 Sealed-interface redeemer with switch (auction) ```java @SpendingValidator public class AuctionValidator { record AuctionDatum(byte[] seller, BigInteger reservePrice) {} sealed interface AuctionAction permits Bid, Close {} record Bid(byte[] bidder, BigInteger amount) implements AuctionAction {} record Close() implements AuctionAction {} @Entrypoint public static boolean validate(AuctionDatum datum, AuctionAction redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); return switch (redeemer) { case Bid b -> { boolean bidderSigned = txInfo.signatories().contains(PubKeyHash.of(b.bidder())); boolean meetsReserve = b.amount().compareTo(datum.reservePrice()) >= 0; yield bidderSigned && meetsReserve; } case Close c -> txInfo.signatories().contains(PubKeyHash.of(datum.seller())); }; } } ``` ##### 10.3 Parameterized minting policy (one-shot mint) ```java import com.bloxbean.cardano.julc.stdlib.annotation.MintingValidator; import com.bloxbean.cardano.julc.stdlib.annotation.Param; import com.bloxbean.cardano.julc.stdlib.Builtins; @MintingValidator public class OneShotMintPolicy { @Param static byte[] utxoTxId; @Param static BigInteger utxoIndex; @Entrypoint public static boolean validate(PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); boolean found = false; for (var input : txInfo.inputs()) { var ref = input.outRef(); byte[] refTxIdBytes = Builtins.toByteString(ref.txId()); if (Builtins.equalsByteString(refTxIdBytes, utxoTxId) && ref.index().compareTo(utxoIndex) == 0) { found = true; break; } } return found; } } ``` ##### 10.4 Output checking with OutputLib (escrow) ```java @SpendingValidator public class OutputCheckValidator { record PaymentDatum(Address recipientAddress, BigInteger minAmount) {} @Entrypoint public static boolean validate(PaymentDatum datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); JulcList outputs = txInfo.outputs(); BigInteger paid = OutputLib.lovelacePaidTo(outputs, datum.recipientAddress()); return paid.compareTo(datum.minAmount()) >= 0; } } ``` ##### 10.5 HOF with lambda + variable capture (token distribution) ```java record BeneficiaryEntry(byte[] pkh, BigInteger amount) {} record DistributionDatum(JulcList beneficiaries) {} static boolean allBeneficiariesPaid(JulcList beneficiaries, JulcList outputs) { return beneficiaries.all(entry -> { BigInteger paid = totalPaidTo(outputs, entry.pkh()); return paid.compareTo(entry.amount()) >= 0; }); } static BigInteger totalPaidTo(JulcList outputs, byte[] targetPkh) { JulcList matching = outputs.filter(out -> { Credential cred = out.address().credential(); return switch (cred) { case Credential.PubKeyCredential pk -> Builtins.equalsByteString(Builtins.toByteString(pk.hash()), targetPkh); case Credential.ScriptCredential sc -> false; }; }); BigInteger total = BigInteger.ZERO; for (var out : matching) { total = total.add(out.value().lovelaceOf()); } return total; } ``` ##### 10.6 Reusable on-chain library ```java @OnchainLibrary public class ValidationUtils { public static boolean isAfterDeadline(TxInfo txInfo, BigInteger deadline) { return IntervalLib.contains(txInfo.validRange(), deadline); } public static boolean hasSigner(TxInfo txInfo, byte[] pkh) { return txInfo.signatories().contains(PubKeyHash.of(pkh)); } public static boolean hasMinLovelace(Value value, BigInteger minLovelace) { return value.lovelaceOf().compareTo(minLovelace) >= 0; } } ``` ##### 10.7 More real-world examples For deeper patterns, consult [`julc-examples`](https://github.com/bloxbean/julc-examples): - `cftemplates/` — Cardano Foundation template patterns (treasury, etc.). - `nft/Cip68Nft.java` — CIP-68 NFT minting with metadata. - `uverify/` — multi-script UVerify protocol. - `mpf/MerklePatriciaForestry.java` — Merkle Patricia Forestry verification (recursive helpers). - `linkedlist/` — on-chain linked list pattern. - `swap/` — atomic swap. - `lending/` — lending protocol primitives. **Machine-readable index for AI retrieval:** [`/ai/examples.json`](/ai/examples.json) — every example tagged with `id`, `difficulty`, `concepts`, `cipRelevance`, and `kind`. Use this to deterministically answer "show me X-pattern examples" without prose-matching. --- #### 11. Anti-patterns (do NOT do these) - ❌ **Constructing `PlutusData.ConstrData/IntData/BytesData/MapData/ListData` in on-chain code.** Use records, sealed interfaces, `BigInteger`, `byte[]`, `JulcList`, `JulcMap`. - ❌ **`(PubKeyHash)(Object) bytes`** — use `PubKeyHash.of(bytes)`. - ❌ **Storing lambdas in variables.** Pass them inline to HOFs. - ❌ **`return` inside a `while` loop.** Use a boolean flag. - ❌ **`var x;` then `x = 5;`.** Initialize at declaration. - ❌ **Same name for switch case field and method parameter.** Rename one. - ❌ **`a == b` for `BigInteger` or `String`.** Use `.compareTo` or `.equals`. - ❌ **`null`.** Use `Optional` or sealed interfaces. - ❌ **`throw new Exception(...)`.** Use `Builtins.error()` or return `false`. - ❌ **`@Param PlutusData.BytesData/MapData/ListData/IntData`.** Banned. - ❌ **Three-way mutual recursion.** Refactor. - ❌ **Calling `.hash()` on a value that is already hash bytes.** Causes double-unwrap. --- #### 12. Tooling (for the agent's awareness) - **`julc` CLI** — `julc new`, `julc build`, `julc check`, `julc repl`, `julc eval`. - **Gradle plugin** — `julc-annotation-processor` is the primary build path; produces `META-INF/plutus/.plutus.json`. - **julc-testkit** — `ValidatorTest.compileValidatorByName(fqcn)`, `evaluate(...)`, `assertSuccess(...)`, plus property-based fuzzing. - **Off-chain integration** — `JulcScriptLoader.load(MyValidator.class)` returns a `PlutusV3Script` usable with cardano-client-lib's `QuickTxBuilder`. For a full reference, see [/getting-started/](/getting-started/) and [/reference/api-reference/](/reference/api-reference/). --- #### 13. Quick checklist before generating JuLC code Before writing any JuLC code, verify the agent can answer "yes" to each: 1. ✅ Am I using `record`s and sealed interfaces, **not** raw `PlutusData.ConstrData/IntData/BytesData/MapData/ListData`? 2. ✅ Are all my variables initialized at declaration? 3. ✅ Have I avoided `return` inside `while` loops? 4. ✅ Are my switch cases exhaustive (or do I have a `default ->` branch)? 5. ✅ Do I use `BigInteger.compareTo` / `BigInteger.equals`, not `==`? 6. ✅ Am I passing lambdas inline to HOFs (not storing them)? 7. ✅ Have I used `PubKeyHash.of(bytes)` style factories instead of `(PubKeyHash)(Object) bytes` casts? 8. ✅ Are switch case binding names different from method parameter names? 9. ✅ Am I using `@SpendingValidator` / `@MintingValidator` etc. correctly with a `static @Entrypoint` method? 10. ✅ Have I imported from `com.bloxbean.cardano.julc.ledger.*` and `com.bloxbean.cardano.julc.stdlib.lib.*`? If yes to all → write the code. If unsure on any → re-read the relevant section above. --- *This pack is generated and maintained as part of [ADR-020](https://github.com/bloxbean/julc/blob/main/adr/020-ai-ready-developer-experience.md). Failures in the wild should result in updates here.* --- ## MCP closed-loop walkthrough Source: https://julc.dev/ai/transcripts/closed-loop-walkthrough/ > Real captured transcript of an AI agent compiling, fixing, and evaluating a JuLC vesting validator through the julc mcp server. This is a verbatim transcript of a JSON-RPC session against `julc mcp` showing the full AI feedback loop: lint pre-flight → broken compile → diagnostic → substantive fix → clean compile → evaluate. Captured during Phase C6 of the AI-readiness rollout (ADR-020). The session uses one stdio connection. Each step's request/response is summarized; the raw JSON-RPC stream is committed alongside this page at [`closed-loop-walkthrough.json`](/ai/transcripts/closed-loop-walkthrough.json). #### 1. `initialize` — handshake ```json {"jsonrpc":"2.0","id":1,"method":"initialize", "params":{"protocolVersion":"2024-11-05","capabilities":{}, "clientInfo":{"name":"e2e-v2","version":"0.1"}}} ``` Server replies with `julc-mcp v0.1.0-…` plus a *server instructions* preamble that all clients can read — it points to the starter pack and lists the critical type-class rule, the no-`return`-in-`while` rule, and the `Optional.of/empty` API. #### 2. `tools/list` ``` - julc_ping — round-trip check - julc_compile — Compile JuLC source - julc_lint — Pre-compile JuLC lint - julc_evaluate — Evaluate a JuLC method ``` #### 3. `julc_lint` — catches `Optional.mkNone()` before compile The agent generated: ```java public class V { static Object none() { return Optional.mkNone(); } } ``` `julc_lint` returns a single finding: ```json { "rule": "JULC-LINT-OPTIONAL-API", "level": "error", "message": "Optional.mkNone(...) is not a JuLC API. The real factories are Optional.of(x) and Optional.empty().", "suggestion": "Replace `Optional.mkNone()` with `Optional.empty()`." } ``` Without this rule, the agent would have hit a generic "unknown method" compile error and not known which substitution to make. #### 4. `julc_compile` — first attempt fails on `return` inside a `while` loop The agent's first cut at a vesting validator manually iterates to check the deadline and uses `return` inside `while`: ```java @SpendingValidator public class VestingV1 { record VestingDatum(byte[] beneficiary, BigInteger deadline) {} record VestingRedeemer() {} @Entrypoint public static boolean validate(VestingDatum d, VestingRedeemer r, ScriptContext ctx) { var i = BigInteger.ZERO; while (i.compareTo(d.deadline()) < 0) { if (i.equals(d.deadline())) return true; // ← rejected i = i.add(BigInteger.ONE); } return false; } } ``` Server returns: ```json { "ok": false, "diagnostics": [ { "level": "error", "message": "VestingV1.java:13:5: 'return' is not supported inside while loop body\n Hint: Use 'break' to exit the loop early, or restructure to assign to a flag and return after the loop.", "line": 13, "column": 5 } ] } ``` (This site does not yet emit a `JULC####` code — see Phase C/E follow-up. The message + hint are still actionable; the agent has everything it needs to fix the code.) #### 5. `julc_compile` — substantive fix succeeds The agent realizes the manual loop is wrong on two fronts: `return` inside `while` is rejected, and the loop is conceptually unnecessary — the validator should check the *transaction's signatories* and let the time check happen via the script's validity range. Idiomatic JuLC: ```java @SpendingValidator public class VestingV2 { record VestingDatum(byte[] beneficiary, BigInteger deadline) {} record VestingRedeemer() {} @Entrypoint public static boolean validate(VestingDatum d, VestingRedeemer r, ScriptContext ctx) { return ctx.txInfo().signatories().contains(PubKeyHash.of(d.beneficiary())); } } ``` Server returns: ```json { "ok": true, "diagnostics": [], "scriptSizeBytes": 162, "scriptSizeFormatted": "162 B" } ``` The agent now has a working validator — and it's *idiomatic*: typed datum/redeemer records, `PubKeyHash.of(bytes)` factory (not `(PubKeyHash)(Object) bytes`), no manual loop, no `Optional.mkNone`. Total round-trips: lint (1) + compile-fail (1) + compile-clean (1) = 3 roundtrips, no human in the loop. #### 6. `julc_evaluate` — sanity-check a pure helper For confidence, the agent ran a pure helper: ```java import java.math.BigInteger; public class M { public static BigInteger sq(BigInteger x) { return x.multiply(x); } } ``` ```json { "name": "julc_evaluate", "arguments": { "source": "...", "method": "sq", "args": [{"int": 7}] } } ``` Response: ```json { "ok": true, "result": "49", "resultType": "integer", "cpu": 367797, "memory": 1734, "traces": [] } ``` Done. The closed loop works end-to-end. #### How to reproduce 1. Build the CLI: `./gradlew :julc-cli:installDist` from the repo root (or `brew install bloxbean/tap/julc`). 2. Configure your AI tool — see the [setup guide](/ai/) for Claude Code / Claude Desktop / Cursor / Continue snippets. 3. Or replay the raw transcript yourself: ```bash ( cat closed-loop-walkthrough.json; sleep 5 ) \ | julc mcp ``` (The JSON file is committed alongside this markdown for reproducibility.) #### What this demonstrates - **The lint engine catches what the compiler accepts (or fails confusingly on).** `Optional.mkNone()` doesn't exist as JuLC API but is plausible-looking enough that LLMs frequently invent it. The lint rule fires before compile so the agent never wastes a compile cycle on it. - **Compile diagnostics are actionable.** The `return`-inside-`while` error includes a `Hint:` and a clear file:line. Even without a `JULC####` code on every site (yet), the agent has enough to drive the fix. - **The agent's fix is type-class idiomatic.** No raw PlutusData. Uses `PubKeyHash.of(...)`, typed records, `ctx.txInfo().signatories().contains(...)`. Matches every rule in the AI starter pack. - **Evaluation closes the loop.** Agents can run pure helpers as a sanity check — no need to scaffold a test class. For the full set of MCP tools and their schemas, run `julc mcp` and call `tools/list` (or look at the JSON returned by `julc_compile` etc. in your client of choice). --- # Overview --- ## JuLC Documentation Source: https://julc.dev/overview/ > Comprehensive documentation for JuLC — the Java UPLC Compiler for Cardano Welcome to the JuLC documentation. JuLC compiles a safe subset of Java to Plutus V3 UPLC for Cardano smart contracts. #### New to JuLC? #### Guides #### Standard Library #### Reference #### Internals For compiler contributors: #### Example Repositories - [julc-helloworld](https://github.com/bloxbean/julc-helloworld) — Simple vesting contract with tests - [julc-examples](https://github.com/bloxbean/julc-examples) — Advanced validators and patterns --- # Getting Started --- ## Write Your First JuLC Contract Source: https://julc.dev/first-contract/ > Step-by-step guide to scaffolding a project, writing a validator, compiling, and testing with the julc CLI This tutorial walks you through writing your first Cardano smart contract with JuLC. You will install the CLI, scaffold a project, write a spending validator, compile it to UPLC, and test it locally — all in under 10 minutes. #### Prerequisites - **Java 25+** (GraalVM recommended) - **julc CLI** installed (see below) #### 1. Install the julc CLI ##### Homebrew (macOS / Linux) ```bash brew install bloxbean/tap/julc ``` ##### Direct download Download from [GitHub Releases](https://github.com/bloxbean/julc/releases). On macOS, remove the quarantine attribute after download: ```bash xattr -d com.apple.quarantine ./julc chmod +x ./julc ``` Verify the installation: ```bash julc --version ``` --- #### 2. Scaffold a Project The `julc new` command creates a ready-to-use project. JuLC supports three project types: ##### Option A: Basic project (julc CLI only) The simplest option — no build tool required. The julc CLI handles compilation and testing directly. ```bash julc new my-first-contract ``` This creates: ``` my-first-contract/ julc.toml # Project config src/ AlwaysSucceeds.java # Starter validator test/ AlwaysSucceedsTest.java # Starter test .julc/stdlib/ # Stdlib sources (auto-installed) .idea/ # IntelliJ project files ``` Build and test with: ```bash cd my-first-contract julc build # Compile validators to UPLC julc check # Run on-chain tests ``` ##### Option B: Gradle project For teams using Gradle. Includes Gradle wrapper, annotation processor, JUnit 5, and cardano-client-lib integration. ```bash julc new my-contract -t gradle ``` You'll be prompted for: - **Group ID** (default: `com.example`) - **Artifact ID** (default: project name) - **Package name** (default: `.`) This creates a standard Gradle project: ``` my-contract/ build.gradle # Pre-configured with JuLC deps settings.gradle gradlew / gradlew.bat # Gradle wrapper src/main/java/com/example/my_contract/ AlwaysSucceeds.java # Starter validator src/test/java/com/example/my_contract/ AlwaysSucceedsTest.java # JUnit 5 test with ContractTest ``` Build and test with: ```bash cd my-contract ./gradlew build ``` ##### Option C: Maven project For teams using Maven. Includes Maven wrapper, annotation processor config, and full pom.xml. ```bash julc new my-contract -t maven ``` Same prompts as Gradle (Group ID, Artifact ID, Package name). Build and test with: ```bash cd my-contract ./mvnw compile test ``` --- #### 3. Write a Spending Validator Open the generated `AlwaysSucceeds.java` and replace it with a real vesting validator: ```java package com.example.my_contract; import com.bloxbean.cardano.julc.stdlib.annotation.SpendingValidator; import com.bloxbean.cardano.julc.stdlib.annotation.Entrypoint; import com.bloxbean.cardano.julc.ledger.*; import com.bloxbean.cardano.julc.core.PlutusData; import com.bloxbean.cardano.julc.core.types.PubKeyHash; import java.math.BigInteger; @SpendingValidator public class VestingValidator { // Datum: who can claim and when record VestingDatum(PubKeyHash beneficiary, BigInteger deadline) {} @Entrypoint static boolean validate(VestingDatum datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); // The beneficiary must have signed the transaction boolean signed = txInfo.signatories() .contains(datum.beneficiary()); // The deadline must have passed boolean pastDeadline = datum.deadline() .compareTo(BigInteger.ZERO) > 0; return signed && pastDeadline; } } ``` Key concepts: - **`@SpendingValidator`** — marks this class as a spending validator - **`record VestingDatum`** — defines the on-chain datum structure - **`@Entrypoint`** — the method the Cardano node calls to validate a transaction - **`ScriptContext`** — provides typed access to the transaction being validated --- #### 4. Compile to UPLC ##### With julc CLI (basic project) ```bash julc build ``` Output: ``` Building my-first-contract ... Compiling VestingValidator ... OK [245 bytes, a3f8b2c1...] Build successful: 1 validator(s) compiled to build/plutus/ ``` The compiled artifacts are in `build/plutus/`: - `VestingValidator.uplc` — the UPLC program (human-readable) - `plutus.json` — CIP-57 blueprint (for deployment tools) ##### With Gradle ```bash ./gradlew build ``` The annotation processor compiles during `javac`. The compiled script is bundled into the JAR at `META-INF/plutus/` and can be loaded at runtime: ```java PlutusV3Script script = JulcScriptLoader.load(VestingValidator.class); ``` ##### With Maven ```bash ./mvnw compile ``` Same annotation processor behavior as Gradle. --- #### 5. Test Your Validator ##### With julc CLI (basic project) Create `test/VestingValidatorTest.java`: ```java import com.bloxbean.cardano.julc.stdlib.test.Test; public class VestingValidatorTest { @Test public static boolean test_beneficiary_can_claim() { // On-chain test — evaluated as UPLC return true; } } ``` Run: ```bash julc check ``` ##### With Gradle / Maven (JUnit 5) The scaffolded test extends `ContractTest` which provides helpers: ```java package com.example.my_contract; import com.bloxbean.cardano.julc.core.PlutusData; import com.bloxbean.cardano.julc.testkit.ContractTest; import com.bloxbean.cardano.julc.testkit.TestDataBuilder; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class VestingValidatorTest extends ContractTest { @Test void testBeneficiaryCanClaim() { // Compile with source maps for better error messages var program = compileValidatorWithSourceMap(VestingValidator.class); // Build a mock ScriptContext var ref = TestDataBuilder.randomTxOutRef_typed(); var ctx = spendingContext(ref, PlutusData.UNIT) .redeemer(PlutusData.UNIT) .buildPlutusData(); // Evaluate the validator var result = evaluateWithTrace(program, ctx); // Check result assertTrue(result.isSuccess()); // Print execution details System.out.print(formatExecutionTrace()); System.out.print(formatBudgetSummary()); } } ``` Run: ```bash ./gradlew test # Gradle ./mvnw test # Maven ``` --- #### 6. Explore with the REPL The julc REPL lets you experiment with on-chain expressions interactively: ```bash julc repl ``` ``` JuLC REPL v0.1.0 — type :help for commands julc> 1 + 2 => 3 CPU: 230,100 Mem: 602 julc> ListsLib.length(List.of(1, 2, 3)) => 3 CPU: 1,082,720 Mem: 3,410 julc> MathLib.pow(2, 10) => 1024 CPU: 4,510,306 Mem: 12,818 julc> :doc ListsLib.filter ListsLib.filter(list, predicate) Keep only elements matching predicate. Returns: JulcList ``` Every expression is compiled to UPLC and evaluated — CPU and memory budgets are shown in real time. --- #### 7. Inspect Blueprints After building, inspect the compiled blueprint: ```bash julc blueprint inspect build/plutus/plutus.json ``` This shows the validator hash, script size, and parameter info — useful for computing script addresses and preparing transactions. --- #### CLI Command Reference | Command | Description | |---------|-------------| | `julc new ` | Create a new project (`-t basic`, `-t gradle`, `-t maven`) | | `julc build` | Compile validators to UPLC + generate CIP-57 blueprint | | `julc check` | Discover and run on-chain tests | | `julc repl` | Interactive REPL with live budgets | | `julc eval ` | Evaluate a single expression | | `julc blueprint inspect ` | Inspect a compiled blueprint | | `julc blueprint address ` | Compute script address from blueprint | | `julc uplc decode ` | Decode UPLC from hex/CBOR | | `julc uplc fmt ` | Pretty-print UPLC | | `julc install` | Install/update stdlib sources | | `julc --version` | Show version | --- #### Next Steps - [Getting Started](/getting-started/) — comprehensive guide covering all language features - [Standard Library Reference](/stdlib/stdlib-guide/) — all 13 stdlib libraries - [Testing Guide](/guides/testing-guide/) — unit tests, property-based testing, budget analysis - [Examples](/reference/examples/) — 12 example validators --- ## Getting Started with JuLC Source: https://julc.dev/getting-started/ > Getting Started with JuLC - JuLC documentation Write Cardano smart contracts in Java and compile them to Plutus V3 UPLC. JuLC compiles a safe subset of Java to Untyped Plutus Lambda Calculus (UPLC), the on-chain execution language of the Cardano blockchain. You write validators as ordinary Java classes with records, sealed interfaces, and switch expressions. The compiler turns them into efficient Plutus scripts that run on the Cardano CEK machine. #### 1. Prerequisites - **Java 25+** (GraalVM recommended for best performance) - **Gradle 9+** (or Maven 3.9+) - Familiarity with Cardano's eUTxO model and the concept of validators, datums, and redeemers --- #### 2. Project Setup > **Snapshot versions**: Snapshot builds include the Git commit hash in the version string > (e.g. `0.1.0-e0f314e-SNAPSHOT`). > The snapshot repository configuration below is only needed for snapshot versions available in snapshot repository. ##### Build Artifacts After a successful build, for a Gradle project, a validator-specific *.plutus.json file will be generated under the `build/classes/META-INF/plutus` directory. This JSON file contains the compiled UPLC script along with other metadata. If you are writing off-chain code in Java, this file will be automatically loaded by the `JulcScriptLoader.load(VestingValidator.class)` method. There is a JuLC hello world example with `VestingValidator` at https://github.com/bloxbean/julc-helloworld. You can clone this repository and add your validators to get started quickly. ##### Gradle (annotation processor -- primary approach) ```groovy plugins { id 'java' } group = 'com.example' version = '1.0-SNAPSHOT' ext { julcVersion = '' cardanoClientLibVersion = '0.7.1' } repositories { mavenCentral() maven { url "https://central.sonatype.com/repository/maven-snapshots" } } dependencies { // Core: stdlib + ledger types + annotations implementation "com.bloxbean.cardano:julc-stdlib:${julcVersion}" implementation "com.bloxbean.cardano:julc-ledger-api:${julcVersion}" // Annotation processor -- compiles validators during javac annotationProcessor "com.bloxbean.cardano:julc-annotation-processor:${julcVersion}" // Runtime -- load pre-compiled scripts from classpath implementation "com.bloxbean.cardano:julc-cardano-client-lib:${julcVersion}" implementation "com.bloxbean.cardano:cardano-client-lib:${cardanoClientLibVersion}" // Test: VM for local evaluation testImplementation "com.bloxbean.cardano:julc-testkit:${julcVersion}" testImplementation "com.bloxbean.cardano:julc-vm:${julcVersion}" testRuntimeOnly "com.bloxbean.cardano:julc-vm-scalus:${julcVersion}" testImplementation platform('org.junit:junit-bom:5.10.0') testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } test { useJUnitPlatform() } ``` ##### Maven ```xml snapshots-repo https://central.sonatype.com/repository/maven-snapshots false true com.bloxbean.cardano julc-stdlib ${julc.version} com.bloxbean.cardano julc-ledger-api ${julc.version} com.bloxbean.cardano julc-cardano-client-lib ${julc.version} com.bloxbean.cardano cardano-client-lib ${ccl.version} org.apache.maven.plugins maven-compiler-plugin 3.13.0 25 com.bloxbean.cardano julc-annotation-processor ${julc.version} ``` ##### Java version note JuLC uses sealed interfaces, records, pattern matching, and switch expressions. These features are fully standardized since Java 21, so no `--enable-preview` flag is needed with Java 25+. --- #### 3. Your First Validator ##### 3.1 Spending Validator A spending validator guards UTxOs locked at a script address. It receives a datum (the data stored with the UTxO), a redeemer (the data the spender provides), and the full `ScriptContext`. ```java package com.example; import com.bloxbean.cardano.julc.core.PlutusData; import com.bloxbean.cardano.julc.ledger.PubKeyHash; import com.bloxbean.cardano.julc.ledger.ScriptContext; import com.bloxbean.cardano.julc.ledger.TxInfo; import com.bloxbean.cardano.julc.stdlib.annotation.Entrypoint; import com.bloxbean.cardano.julc.stdlib.annotation.SpendingValidator; import java.math.BigInteger; @SpendingValidator public class VestingValidator { record VestingDatum(byte[] beneficiary, BigInteger deadline) {} @Entrypoint static boolean validate(VestingDatum datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); // Check that the beneficiary signed the transaction boolean signed = txInfo.signatories().contains(PubKeyHash.of(datum.beneficiary())); // Check that the deadline has passed (lower bound of valid range > deadline) // Just a dummy check to demonstrate using the datum's deadline field. boolean pastDeadline = datum.deadline().compareTo(BigInteger.ZERO) > 0; return signed && pastDeadline; } } ``` Key points: - `@SpendingValidator` marks the class as a spending validator. - `@Entrypoint` marks the single validation method. It must be `static` and return `boolean`. - The method signature is `(DatumType, RedeemerType, ScriptContext)` for spending validators. - `ctx.txInfo()` gives you typed access to all 16 fields of the V3 `TxInfo`. - `txInfo.signatories()` returns a list of `PubKeyHash` that can be iterated or searched with `.contains()`. - Records like `VestingDatum` compile to Plutus `ConstrData`. Field access (`.beneficiary()`, `.deadline()`) compiles to efficient Data navigation with automatic type unwrapping (`UnBData` for `byte[]`, `UnIData` for `BigInteger`). ##### 3.2 Minting Validator with Sealed Interface Redeemer A minting validator controls which tokens can be minted or burned under a given policy. It receives a redeemer and the `ScriptContext`. ```java package com.example; import com.bloxbean.cardano.julc.stdlib.annotation.MintingValidator; import com.bloxbean.cardano.julc.stdlib.annotation.Entrypoint; import com.bloxbean.cardano.julc.ledger.ScriptContext; import com.bloxbean.cardano.julc.ledger.TxInfo; import com.bloxbean.cardano.julc.core.PlutusData; import java.math.BigInteger; @MintingValidator public class TokenPolicy { sealed interface Action { record Mint(BigInteger amount) implements Action {} record Burn() implements Action {} } @Entrypoint static boolean validate(Action redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); return switch (redeemer) { case Action.Mint m -> { // Only mint positive amounts, and only if authorized boolean positive = m.amount().compareTo(BigInteger.ZERO) > 0; boolean signed = txInfo.signatories().contains( new byte[]{/* authority pkh */}); yield positive && signed; } case Action.Burn b -> { // Anyone can burn their own tokens yield true; } }; } } ``` The sealed interface `Action` compiles to a Plutus `SumType`. The `switch` expression with pattern matching compiles to Data tag inspection with automatic field extraction. The compiler checks exhaustiveness -- every case of the sealed interface must be handled. --- #### 4. Conway Validator Types JuLC supports all six Plutus V3 (Conway era) validator types through purpose- specific annotations: | Annotation | Purpose | Entrypoint Parameters | |---|---|---| | `@SpendingValidator` | Guards spending UTxOs from a script address | `(datum, redeemer, ctx)` or `(redeemer, ctx)` | | `@MintingValidator` | Controls minting/burning of native tokens | `(redeemer, ctx)` | | `@WithdrawValidator` | Authorizes staking reward withdrawals | `(redeemer, ctx)` | | `@CertifyingValidator` | Authorizes delegation certificate operations | `(redeemer, ctx)` | | `@VotingValidator` | Authorizes governance votes (DRep) | `(redeemer, ctx)` | | `@ProposingValidator` | Authorizes governance proposals | `(redeemer, ctx)` | All annotations live in `com.bloxbean.cardano.julc.stdlib.annotation`. > **Deprecation note**: The old `@Validator` and `@MintingPolicy` annotations > still compile but are deprecated. Use `@SpendingValidator` and > `@MintingValidator` for all new code. ##### 4.1 Multi-Validators (@MultiValidator) When a single compiled script needs to handle multiple purposes (e.g. mint **and** spend), annotate the class with `@MultiValidator` instead of a single-purpose annotation. This produces one on-chain script that dispatches on `ScriptInfo` at runtime. **Two modes:** | Mode | How it works | |------|-------------| | **Auto-dispatch** | Multiple `@Entrypoint` methods, each with an explicit `Purpose`. The compiler generates a `ScriptInfo` tag dispatch automatically. | | **Manual dispatch** | A single `@Entrypoint` method with `Purpose.DEFAULT`. You switch on `ctx.scriptInfo()` yourself. | **Purpose enum values:** | Purpose | ScriptInfo tag | ScriptInfo variant | |---------|---------------|-------------------| | `MINT` | 0 | `MintingScript` | | `SPEND` | 1 | `SpendingScript` | | `WITHDRAW` | 2 | `RewardingScript` | | `CERTIFY` | 3 | `CertifyingScript` | | `VOTE` | 4 | `VotingScript` | | `PROPOSE` | 5 | `ProposingScript` | | `DEFAULT` | — | Manual dispatch (no auto-dispatch) | **Entrypoint parameter rules:** | Purpose | Parameters | |---------|-----------| | `SPEND` | 2 params `(redeemer, ctx)` or 3 params `(datum, redeemer, ctx)` — datum is `Optional` or a record type | | All others | 2 params `(redeemer, ctx)` | ###### Auto-dispatch example ```java @MultiValidator public class TokenManager { @Entrypoint(purpose = Purpose.MINT) static boolean mint(PlutusData redeemer, ScriptContext ctx) { // Minting logic return !ctx.txInfo().signatories().isEmpty(); } @Entrypoint(purpose = Purpose.SPEND) static boolean spend(PlutusData redeemer, ScriptContext ctx) { // Spending logic (2-param — no datum) return true; } } ``` The compiler generates ScriptInfo tag dispatch that routes `MintingScript` to `mint()` and `SpendingScript` to `spend()`. Unhandled purposes cause a script Error at runtime. ###### Auto-dispatch with datum (3-param SPEND) ```java @MultiValidator public class TokenManagerWithDatum { @Entrypoint(purpose = Purpose.MINT) static boolean mint(PlutusData redeemer, ScriptContext ctx) { return true; } @Entrypoint(purpose = Purpose.SPEND) static boolean spend(Optional datum, PlutusData redeemer, ScriptContext ctx) { // datum is Optional — Some when present, None when absent return datum.isPresent(); } } ``` ###### Manual dispatch example ```java @MultiValidator public class ManualRouter { @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { return switch (ctx.scriptInfo()) { case ScriptInfo.MintingScript m -> handleMint(redeemer, ctx); case ScriptInfo.SpendingScript s -> handleSpend(redeemer, ctx); case ScriptInfo.RewardingScript r -> handleWithdraw(redeemer, ctx); // Must cover all ScriptInfo variants you handle }; } static boolean handleMint(PlutusData redeemer, ScriptContext ctx) { return true; } static boolean handleSpend(PlutusData redeemer, ScriptContext ctx) { return true; } static boolean handleWithdraw(PlutusData redeemer, ScriptContext ctx) { return true; } } ``` **Validation rules:** - Do not mix `Purpose.DEFAULT` with explicit purposes — use one mode or the other - No duplicate purposes (two `@Entrypoint` methods with the same `Purpose`) - Do not combine `@MultiValidator` with single-purpose annotations (`@SpendingValidator`, etc.) --- #### 5. Data Modeling ##### 5.1 Records Java records compile to Plutus `ConstrData` types. Field access compiles to efficient Data navigation. ```java record Payment(byte[] recipient, BigInteger amount) {} // Construction: compiles to ConstrData(0, [BData(recipient), IData(amount)]) var p = new Payment(recipientBytes, BigInteger.valueOf(1000000)); // Field access: compiles to HeadList/TailList chains + type unwrapping byte[] who = p.recipient(); // UnBData(HeadList(SndPair(UnConstrData(p)))) BigInteger how = p.amount(); // UnIData(HeadList(TailList(SndPair(UnConstrData(p))))) ``` Records can be nested: ```java record Proposal(Payment payment, BigInteger votesNeeded) {} // Access nested fields byte[] target = proposal.payment().recipient(); ``` ##### 5.2 Sealed Interfaces Sealed interfaces compile to Plutus `SumType` (tagged union). Each permitted record variant gets a constructor tag starting from 0. ```java sealed interface Shape { record Circle(BigInteger radius) implements Shape {} // tag 0 record Rectangle(BigInteger w, BigInteger h) implements Shape {} // tag 1 } ``` Use `switch` expressions for exhaustive pattern matching: ```java BigInteger area = switch (shape) { case Shape.Circle c -> c.radius().multiply(c.radius()).multiply(BigInteger.valueOf(3)); case Shape.Rectangle r -> r.w().multiply(r.h()); }; ``` The compiler checks exhaustiveness: if you omit a case for any variant, you get a compile error listing the missing cases. You can also use `instanceof` for type-checking: ```java if (shape instanceof Shape.Circle c) { // c is bound and typed as Circle BigInteger r = c.radius(); } ``` ##### 5.3 @NewType The `@NewType` annotation creates a zero-cost type alias for a single-field record. On-chain, the constructor and `.of()` factory method compile to identity (no `ConstrData` wrapping). ```java import com.bloxbean.cardano.julc.stdlib.annotation.NewType; @NewType record AssetId(byte[] hash) {} // AssetId.of(bytes) compiles to identity -- no ConstrData overhead AssetId id = AssetId.of(someBytes); ``` `@NewType` is `@Retention(RUNTIME)`, `@Target(TYPE)`. The single field must be one of the supported primitive types: - `byte[]` (compiles to `ByteStringType`) - `BigInteger` (compiles to `IntegerType`) - `String` (compiles to `TextType`) - `boolean` (compiles to `BoolType`) Multi-field records or unsupported field types produce a compiler error. ##### 5.4 Tuple2 and Tuple3 Generic tuples are provided in `com.bloxbean.cardano.julc.core.types`: ```java import com.bloxbean.cardano.julc.core.types.Tuple2; import com.bloxbean.cardano.julc.core.types.Tuple3; // Generic type parameters enable auto-unwrap on field access Tuple2 pair = new Tuple2<>(someInt, someBytes); BigInteger first = pair.first(); // auto-generates UnIData byte[] second = pair.second(); // auto-generates UnBData Tuple3 triple = new Tuple3<>(a, b, c); BigInteger third = triple.third(); // auto-generates UnIData ``` On-chain, tuples compile to `ConstrData(0, [first, second, ...])`. With type arguments, field access auto-unwraps based on the generic type (`BigInteger` yields `UnIData`, `byte[]` yields `UnBData`). Raw `Tuple2` or `Tuple3` (without type arguments) defaults to `DataType` for backward compatibility -- no auto-unwrap occurs. **Note**: Tuple2/Tuple3 cannot be used in `switch` expressions because they are registered as `RecordType`, not `SumType`. Use `.first()` and `.second()` field access instead of pattern matching. ##### 5.5 Type.of() Factories Seven ledger hash types provide `.of(byte[])` factory methods that compile to identity on-chain: | Type | Factory | |---|---| | `PubKeyHash` | `PubKeyHash.of(bytes)` | | `ScriptHash` | `ScriptHash.of(bytes)` | | `ValidatorHash` | `ValidatorHash.of(bytes)` | | `PolicyId` | `PolicyId.of(bytes)` | | `TokenName` | `TokenName.of(bytes)` | | `DatumHash` | `DatumHash.of(bytes)` | | `TxId` | `TxId.of(bytes)` | These replace the older `(PubKeyHash)(Object) bytes` cast pattern: ```java // Old pattern (still works but ugly) PubKeyHash pkh = (PubKeyHash)(Object) beneficiaryBytes; // New pattern (recommended) PubKeyHash pkh = PubKeyHash.of(beneficiaryBytes); ``` On-chain, `.of()` is identity (the bytes pass through). Off-chain, it delegates to the record constructor with validation (e.g., `PubKeyHash` checks for exactly 28 bytes). For casting `PlutusData` to typed records (e.g., datums, redeemers), use `PlutusData.cast()`: ```java // Cast raw PlutusData to your datum type MyDatum datum = PlutusData.cast(rawDatumData, MyDatum.class); ``` See the [Advanced Guide](/guides/advanced-guide/#plutusdatacast--clean-type-casting) for full details. --- #### 6. Collections ##### 6.1 Lists Lists (typed as `List` or `JulcList`) support the following instance methods: | Method | Return Type | Description | |---|---|---| | `list.isEmpty()` | `boolean` | True if the list has no elements | | `list.size()` | `long` | Number of elements | | `list.head()` | `T` | First element (error if empty) | | `list.tail()` | `List` | All elements except the first | | `list.get(index)` | `T` | Element at 0-based index | | `list.contains(target)` | `boolean` | True if target is in the list (via EqualsData) | | `list.reverse()` | `List` | Reversed copy | | `list.concat(other)` | `List` | Concatenation of two lists | | `list.take(n)` | `List` | First n elements | | `list.drop(n)` | `List` | All elements after the first n | | `list.prepend(elem)` | `List` | New list with elem at the front | | `list.map(x -> f(x))` | `JulcList` | Apply function to each element | | `list.filter(x -> pred(x))` | `JulcList` | Keep elements matching predicate | | `list.any(x -> pred(x))` | `boolean` | True if any element matches | | `list.all(x -> pred(x))` | `boolean` | True if all elements match | | `list.find(x -> pred(x))` | `T` | First matching element (error if none) | The `prepend` method auto-wraps the element: if you prepend a `BigInteger`, it is automatically wrapped with `IData`; a `byte[]` is wrapped with `BData`. The `map` method wraps each lambda result to `Data`, so the returned list has `PlutusData` elements regardless of input type. To extract typed values from a mapped list, use `Builtins.unIData()` or `Builtins.unBData()` on each element. ```java // Iterate a list with for-each for (TxOut out : ctx.txInfo().outputs()) { // out is typed as TxOut with full field access Value v = out.value(); } ``` ##### 6.2 Maps Maps (typed as `Map` or `JulcMap`) are association lists on-chain. They support the following instance methods: | Method | Return Type | Description | |---|---|---| | `map.get(key)` | `V` | Value associated with key (or error) | | `map.containsKey(key)` | `boolean` | True if key exists | | `map.size()` | `long` | Number of entries | | `map.isEmpty()` | `boolean` | True if map has no entries | | `map.keys()` | `List` | All keys as a list | | `map.values()` | `List` | All values as a list | | `map.insert(k, v)` | `Map` | New map with entry added (shadows existing) | | `map.delete(k)` | `Map` | New map with key removed | Internally, `MapType` variables always hold pair lists (not `MapData`-wrapped values). The `insert` and `delete` operations return pair lists. Iterating over a map with `for-each` yields pairs: ```java // For-each on a map yields PairType entries with .key() and .value() for (var entry : ctx.txInfo().withdrawals()) { Credential cred = entry.key(); BigInteger amount = entry.value(); } ``` ##### 6.3 Optional `Optional` is supported for fields like `TxOut.referenceScript()` and `ScriptInfo.SpendingScript.datum()`: | Method | Description | |---|---| | `opt.isPresent()` | True if a value is present | | `opt.isEmpty()` | True if no value is present | | `opt.get()` | The contained value (error if empty) | On-chain, `Optional` compiles to `Constr(0, [value])` for `Some` and `Constr(1, [])` for `None`. --- #### 7. Typed Ledger Access ##### 7.1 ScriptContext / TxInfo / TxOut The Plutus V3 ScriptContext gives typed access to all transaction fields. **ScriptContext fields:** | Field | Type | Description | |---|---|---| | `ctx.txInfo()` | `TxInfo` | The transaction information | | `ctx.redeemer()` | `PlutusData` | The redeemer provided by the spender | | `ctx.scriptInfo()` | `ScriptInfo` | Information about the executing script | **TxInfo fields (all 16):** | Field | Type | Description | |---|---|---| | `txInfo.inputs()` | `JulcList` | Inputs being consumed | | `txInfo.referenceInputs()` | `JulcList` | Reference inputs (read-only) | | `txInfo.outputs()` | `JulcList` | Transaction outputs | | `txInfo.fee()` | `BigInteger` | Transaction fee in lovelace | | `txInfo.mint()` | `Value` | Minted/burned tokens | | `txInfo.certificates()` | `JulcList` | Delegation certificates | | `txInfo.withdrawals()` | `JulcMap` | Staking withdrawals | | `txInfo.validRange()` | `Interval` | Validity time range | | `txInfo.signatories()` | `JulcList` | Transaction signers | | `txInfo.redeemers()` | `JulcMap` | All redeemers | | `txInfo.datums()` | `JulcMap` | Datum witness table | | `txInfo.id()` | `TxId` | Transaction hash | | `txInfo.votes()` | `JulcMap>` | Governance votes | | `txInfo.proposalProcedures()` | `JulcList` | Governance proposals | | `txInfo.currentTreasuryAmount()` | `Optional` | Current treasury | | `txInfo.treasuryDonation()` | `Optional` | Treasury donation | **TxOut fields:** | Field | Type | Description | |---|---|---| | `txOut.address()` | `Address` | Destination address | | `txOut.value()` | `Value` | The value carried | | `txOut.datum()` | `OutputDatum` | Attached datum (None, Hash, or Inline) | | `txOut.referenceScript()` | `Optional` | Optional reference script | ##### 7.2 Value A `Value` represents a multi-asset value: `Map>`. Instance methods available on-chain: | Method | Return Type | Description | |---|---|---| | `value.lovelaceOf()` | `BigInteger` | ADA amount (in lovelace) | | `value.isEmpty()` | `boolean` | True if value has no entries | | `value.assetOf(policy, token)` | `BigInteger` | Amount of a specific token | **Caveat**: `Value.assetOf()` uses `EqualsData` internally. If you pass `byte[]` arguments for policy/token, they must be wrapped with `Builtins.bData()` first. Otherwise `EqualsData(BData(...), ByteString(...))` fails at runtime: ```java // Correct: wrap byte[] args with bData BigInteger amount = value.assetOf( Builtins.bData(policyIdBytes), Builtins.bData(tokenNameBytes)); // Or use ValuesLib.assetOf which handles wrapping for you: BigInteger amount = ValuesLib.assetOf(value, policyIdBytes, tokenNameBytes); ``` ##### 7.3 ScriptInfo `ScriptInfo` is a sealed interface describing the currently executing script: | Variant | Fields | Constructor Tag | |---|---|---| | `MintingScript` | `policyId: PolicyId` | 0 | | `SpendingScript` | `txOutRef: TxOutRef, datum: Optional` | 1 | | `RewardingScript` | `credential: Credential` | 2 | | `CertifyingScript` | `index: BigInteger, cert: TxCert` | 3 | | `VotingScript` | `voter: Voter` | 4 | | `ProposingScript` | `index: BigInteger, procedure: ProposalProcedure` | 5 | Use switch to dispatch: ```java return switch (ctx.scriptInfo()) { case ScriptInfo.MintingScript ms -> handleMint(ms.policyId()); case ScriptInfo.SpendingScript ss -> handleSpend(ss.txOutRef()); case ScriptInfo.RewardingScript rs -> handleReward(rs.credential()); case ScriptInfo.CertifyingScript cs -> handleCert(cs.cert()); case ScriptInfo.VotingScript vs -> handleVote(vs.voter()); case ScriptInfo.ProposingScript ps -> handlePropose(ps.procedure()); }; ``` ##### 7.4 Address and Credential `Address` is a record with a payment credential and an optional staking credential: ```java // Address fields Credential paymentCred = address.credential(); Optional stakingCred = address.stakingCredential(); ``` `Credential` is a sealed interface with two variants: | Variant | Fields | Tag | |---|---|---| | `PubKeyCredential` | `hash: PubKeyHash` | 0 | | `ScriptCredential` | `hash: ScriptHash` | 1 | ```java return switch (address.credential()) { case Credential.PubKeyCredential pk -> { byte[] pkh = (byte[])(Object) pk.hash(); yield checkSigner(pkh); } case Credential.ScriptCredential sc -> { byte[] sh = (byte[])(Object) sc.hash(); yield checkScript(sh); } }; ``` --- #### 8. Control Flow ##### 8.1 If/Else and Ternary Standard if/else compiles to Plutus `IfThenElse`: ```java if (amount.compareTo(BigInteger.ZERO) > 0) { return true; } else { return false; } // Ternary also works boolean valid = amount.compareTo(BigInteger.ZERO) > 0 ? true : false; ``` Both branches must be present -- an `if` without `else` is supported only as a statement (not an expression). ##### 8.2 Switch Expressions Switch expressions on sealed interfaces compile to Data tag inspection with automatic field extraction: ```java BigInteger result = switch (action) { case Action.Mint m -> m.amount(); case Action.Burn b -> BigInteger.ZERO; }; ``` The compiler checks exhaustiveness: if you omit a case, you get a compile error listing the missing variants. ##### 8.3 instanceof Pattern Matching ```java if (datum instanceof OutputDatum.OutputDatumInline inline) { PlutusData d = inline.datum(); // use d } ``` ##### 8.4 For-Each Loops For-each loops over lists are desugared into tail-recursive functions with accumulators. The loop body can update one or more accumulator variables. ```java // Single accumulator long count = 0; for (TxOut out : ctx.txInfo().outputs()) { count = count + 1; } // Multi-accumulator long total = 0; boolean found = false; for (TxInInfo input : ctx.txInfo().inputs()) { total = total + 1; if (someCondition(input)) { found = true; } else { found = found; } } // Break: set the cursor to empty list to exit early boolean exists = false; for (PubKeyHash sig : txInfo.signatories()) { if (Builtins.equalsByteString(sig.hash(), targetPkh)) { exists = true; } else { exists = exists; } } ``` For full loop patterns, accumulator rules, and examples, see [for-loop-patterns.md](/guides/for-loop-patterns/). ##### 8.5 While Loops While loops also desugar to tail-recursive functions: ```java var current = list; long count = 0; while (!Builtins.nullList(current)) { count = count + 1; current = Builtins.tailList(current); } ``` For details, see [for-loop-patterns.md](/guides/for-loop-patterns/). ##### 8.6 Nested Loops Nested loops are supported: while-in-while, for-each-in-for-each, and mixed nesting all work. Each loop gets a unique LetRec name, and inner-loop results are correctly rebound into outer-loop accumulators. ```java long totalOutputs = 0; for (TxInInfo input : ctx.txInfo().inputs()) { long innerCount = 0; for (TxOut out : ctx.txInfo().outputs()) { innerCount = innerCount + 1; } totalOutputs = totalOutputs + innerCount; } ``` ##### 8.7 What Does Not Work - **No `continue` statement** -- every branch must assign all accumulators - **No C-style `for(init; cond; step)`** -- use `while` or for-each - **No `do-while`** -- use `while` with an initial check - **No `return` inside multi-accumulator loop body** -- the loop must complete naturally; use a boolean accumulator for early-exit logic --- #### 9. Standard Library All standard library classes live in `com.bloxbean.cardano.julc.stdlib.lib` and are annotated with `@OnchainLibrary`. They are automatically discovered and compiled when your validator references them. | Library | Description | |---|---| | `ContextsLib` | ScriptContext/TxInfo field accessors, `signedBy`, `findOwnInput`, `getContinuingOutputs`, `findDatum`, `ownHash`, `trace` | | `ListsLib` | `empty`, `prepend`, `length`, `isEmpty`, `head`, `tail`, `reverse`, `concat`, `nth`, `take`, `drop`, `contains`, `containsInt`, `containsBytes`, `hasDuplicateInts`, `hasDuplicateBytes` + PIR HOFs (`any`, `all`, `find`, `foldl`, `map`, `filter`, `zip`) | | `ValuesLib` | `lovelaceOf`, `assetOf`, `containsPolicy`, `geq`, `geqMultiAsset`, `leq`, `eq`, `isZero`, `singleton`, `negate`, `flatten`, `flattenTyped`, `add`, `subtract`, `countTokensWithQty`, `findTokenName` | | `MapLib` | `lookup`, `member`, `insert`, `delete`, `keys`, `values`, `toList`, `fromList`, `size` | | `OutputLib` | `txOutAddress`, `txOutValue`, `txOutDatum`, `outputsAt`, `countOutputsAt`, `uniqueOutputAt`, `outputsWithToken`, `valueHasToken`, `lovelacePaidTo`, `paidAtLeast`, `getInlineDatum`, `resolveDatum`, `findOutputWithToken`, `findInputWithToken` | | `MathLib` | `abs`, `max`, `min`, `divMod`, `quotRem`, `pow`, `sign`, `expMod` | | `IntervalLib` | `contains`, `always`, `after`, `before`, `between`, `never`, `isEmpty`, `finiteUpperBound`, `finiteLowerBound` | | `CryptoLib` | `sha2_256`, `blake2b_256`, `sha3_256`, `blake2b_224`, `keccak_256`, `verifyEd25519Signature`, `verifyEcdsaSecp256k1`, `verifySchnorrSecp256k1`, `ripemd_160` (all hash functions also available via `Builtins.*`) | | `ByteStringLib` | `at`, `cons`, `slice`, `length`, `drop`, `take`, `append`, `empty`, `zeros`, `equals`, `lessThan`, `lessThanEquals`, `integerToByteString`, `byteStringToInteger`, `encodeUtf8`, `decodeUtf8`, `serialiseData`, `hexNibble`, `toHex`, `intToDecimalString`, `utf8ToInteger` | | `BitwiseLib` | `andByteString`, `orByteString`, `xorByteString`, `complementByteString`, `readBit`, `writeBits`, `shiftByteString`, `rotateByteString`, `countSetBits`, `findFirstSetBit` | | `AddressLib` | `credentialHash`, `isScriptAddress`, `isPubKeyAddress`, `paymentCredential` | For full method signatures and usage examples, see [stdlib-guide.md](/stdlib/stdlib-guide/). --- #### 10. User Libraries (@OnchainLibrary) You can write your own on-chain libraries that are auto-discovered by the compiler. ##### Creating a library ```java package com.example.lib; import com.bloxbean.cardano.julc.stdlib.annotation.OnchainLibrary; import com.bloxbean.cardano.julc.stdlib.Builtins; import java.math.BigInteger; @OnchainLibrary public class MyLib { public static BigInteger doubleAmount(BigInteger x) { return x.add(x); } public static boolean isPositive(BigInteger x) { return x.compareTo(BigInteger.ZERO) > 0; } } ``` ##### Using a library from a validator ```java @SpendingValidator public class MyValidator { @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { BigInteger doubled = MyLib.doubleAmount(BigInteger.valueOf(21)); return MyLib.isPositive(doubled); } } ``` ##### Static field initializers Static fields with initializers in `@OnchainLibrary` classes compile as `Let` bindings: ```java @OnchainLibrary public class Constants { static BigInteger THRESHOLD = BigInteger.valueOf(1000000); public static boolean meetsThreshold(BigInteger amount) { return amount.compareTo(THRESHOLD) >= 0; } } ``` ##### Cross-library calls Libraries can call other libraries. The compiler resolves dependencies transitively. When publishing a library as a JAR, the Gradle plugin bundles the Java source under `META-INF/plutus-sources/` so consumers can auto-discover and compile it. For full details, see [library-developer-guide.md](/reference/library-developer-guide/). --- #### 11. Parameterized Validators (@Param) Parameterized validators have fields that are "baked in" at deploy time via UPLC partial application. Each unique set of parameter values produces a different script hash/address. ```java import com.bloxbean.cardano.julc.stdlib.annotation.SpendingValidator; import com.bloxbean.cardano.julc.stdlib.annotation.Entrypoint; import com.bloxbean.cardano.julc.stdlib.annotation.Param; import com.bloxbean.cardano.julc.ledger.ScriptContext; import com.bloxbean.cardano.julc.core.PlutusData; import java.math.BigInteger; @SpendingValidator public class ParameterizedVesting { @Param PlutusData beneficiary; @Param PlutusData deadline; @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { return ctx.txInfo().signatories().contains(beneficiary); } } ``` **Critical**: `@Param` fields must always use `PlutusData` as their type, never `PlutusData.BytesData`, `byte[]`, or other specific types. `@Param` values are always raw `Data` at runtime, and using a specific type causes the compiler to generate incorrect conversion code. Parameters are applied in declaration order when loading: ```java PlutusV3Script script = JulcScriptLoader.load(ParameterizedVesting.class, BytesPlutusData.of(ownerPkh), // beneficiary BigIntPlutusData.of(deadlineMs)); // deadline ``` --- #### 12. Lambda Expressions and HOFs The standard library provides higher-order functions that accept lambda expressions. These are registered as PIR-level functions in the `StdlibRegistry`. ##### ListsLib HOFs ```java import com.bloxbean.cardano.julc.stdlib.lib.ListsLib; // map: transform each element var doubled = ListsLib.map(amounts, x -> x.multiply(BigInteger.TWO)); // filter: keep elements matching a predicate var positives = ListsLib.filter(amounts, x -> x.compareTo(BigInteger.ZERO) > 0); // foldl: left fold with accumulator BigInteger sum = ListsLib.foldl(amounts, BigInteger.ZERO, (acc, x) -> acc.add(x)); // any: true if any element matches boolean hasLarge = ListsLib.any(amounts, x -> x.compareTo(BigInteger.valueOf(1000)) > 0); // all: true if all elements match boolean allPositive = ListsLib.all(amounts, x -> x.compareTo(BigInteger.ZERO) > 0); // find: return first matching element (as Optional-encoded Data) PlutusData found = ListsLib.find(items, x -> someCondition(x)); // zip: combine two lists pairwise var zipped = ListsLib.zip(listA, listB); ``` ##### Instance HOF methods Lists also support HOF methods as instance calls. Lambda parameter types are auto-inferred from the list element type: ```java // Instance methods -- equivalent to the static calls above var doubled = amounts.map(x -> x.multiply(BigInteger.TWO)); var positives = amounts.filter(x -> x.compareTo(BigInteger.ZERO) > 0); boolean hasLarge = amounts.any(x -> x.compareTo(BigInteger.valueOf(1000)) > 0); boolean allPositive = amounts.all(x -> x.compareTo(BigInteger.ZERO) > 0); // Chaining is supported var result = outputs.filter(out -> someCondition(out)).map(out -> transform(out)); // Block-body lambdas work too var processed = items.map(item -> { BigInteger doubled = item.multiply(BigInteger.TWO); return doubled.add(BigInteger.ONE); }); ``` `foldl` is only available as a static call (`ListsLib.foldl`) because it takes two lambda parameters plus an initial value. ##### Lambda syntax Single-expression lambdas: ```java x -> x.add(BigInteger.ONE) ``` Multi-statement lambdas (must have an explicit `return`): ```java (acc, x) -> { BigInteger doubled = x.multiply(BigInteger.TWO); return acc.add(doubled); } ``` **Note**: Lambda `.apply()` is not supported -- you cannot store a lambda in a variable and call it later. Lambdas can only be passed directly to HOF methods. **Note**: Instance HOFs work on lists of `ByteStringType`-mapped types (`JulcList`, `JulcList`, etc.) with both untyped and explicitly-typed lambdas. The compiler automatically avoids double-unwrapping: ```java // Both styles work: signatories.any(sig -> Builtins.equalsByteString((byte[])(Object) sig.hash(), targetPkh)); signatories.any((PubKeyHash sig) -> Builtins.equalsByteString((byte[])(Object) sig.hash(), targetPkh)); ``` --- #### 13. Compiling ##### JulcCompiler (programmatic) ```java import com.bloxbean.cardano.julc.compiler.JulcCompiler; import com.bloxbean.cardano.julc.compiler.CompileResult; import com.bloxbean.cardano.julc.stdlib.StdlibRegistry; // With stdlib support (recommended) var stdlib = StdlibRegistry.defaultRegistry(); var compiler = new JulcCompiler(stdlib::lookup); CompileResult result = compiler.compile(javaSource); if (result.hasErrors()) { System.err.println("Errors: " + result.diagnostics()); } else { var program = result.program(); System.out.println("Script size: " + result.scriptSizeFormatted()); } ``` ##### Annotation Processor (primary approach) The annotation processor compiles validators during `javac`. Add it as shown in Section 2. The processor: 1. Finds classes annotated with `@SpendingValidator`, `@MintingValidator`, `@MultiValidator`, etc. 2. Reads the source file via the compiler's `Trees` API 3. Compiles to UPLC, FLAT-encodes, and double-CBOR-wraps 4. Writes to `META-INF/plutus/.plutus.json` The compiled script ends up on the classpath alongside `.class` files and can be loaded at runtime with `JulcScriptLoader`. ##### Gradle Plugin For projects that prefer separate validator source files, apply the Gradle plugin: ```groovy plugins { id 'com.bloxbean.cardano.julc' version '0.1.0-SNAPSHOT' } ``` Validators in `src/main/plutus/` are compiled during `gradle build` and output to `build/plutus/`. --- #### 14. Testing The `julc-testkit` module provides utilities for compiling and evaluating validators locally without a blockchain. ##### ValidatorTest ```java import com.bloxbean.cardano.julc.testkit.ValidatorTest; import com.bloxbean.cardano.julc.testkit.BudgetAssertions; import com.bloxbean.cardano.julc.core.PlutusData; // Compile from source string var program = ValidatorTest.compile(javaSource); // Compile with stdlib var stdlib = StdlibRegistry.defaultRegistry(); var program = ValidatorTest.compile(javaSource, stdlib::lookup); // Compile a validator class with auto-discovered dependencies var result = ValidatorTest.compileValidator(MyValidator.class); // Evaluate var evalResult = ValidatorTest.evaluate(program, datum, redeemer, ctx); // Assert ValidatorTest.assertValidates(program, datum, redeemer, ctx); ValidatorTest.assertRejects(program, datum, redeemer, ctx); ``` ##### ScriptContextTestBuilder The `ScriptContextTestBuilder` provides a fluent API for constructing test ScriptContexts: ```java import com.bloxbean.cardano.julc.testkit.ScriptContextTestBuilder; import com.bloxbean.cardano.julc.ledger.*; var ref = new TxOutRef(TxId.of(txHashBytes), BigInteger.ZERO); var ctx = ScriptContextTestBuilder.spending(ref) .signer(PubKeyHash.of(pkhBytes)) .input(new TxInInfo(ref, new TxOut(address, value, datum, Optional.empty()))) .output(new TxOut(destAddress, destValue, new OutputDatum.NoOutputDatum(), Optional.empty())) .fee(BigInteger.valueOf(200_000)) .buildPlutusData(); ``` The builder supports three output modes: - `.build()` -- returns a ledger-api `ScriptContext` - `.buildOnchain()` -- returns an on-chain-api `ScriptContext` - `.buildPlutusData()` -- returns `PlutusData` (for direct UPLC evaluation) ##### BudgetAssertions ```java import com.bloxbean.cardano.julc.testkit.BudgetAssertions; var result = ValidatorTest.evaluate(program, ctx); // Check success/failure BudgetAssertions.assertSuccess(result); BudgetAssertions.assertFailure(result); // Check execution budget limits BudgetAssertions.assertBudgetUnder(result, 1_000_000L, 500_000L); // Check trace messages BudgetAssertions.assertTrace(result, "expected message"); BudgetAssertions.assertTraceExact(result, "msg1", "msg2"); BudgetAssertions.assertNoTraces(result); // Check script size var compileResult = ValidatorTest.compileWithDetails(source); BudgetAssertions.assertScriptSizeUnder(compileResult, 16_384); // 16 KB limit ``` ##### JulcEval `JulcEval` lets you test individual helper methods in isolation — no `ScriptContext` required. Use it when you want to verify a single function's logic without building a full validator test scenario. **Factory methods:** | Method | Description | |--------|-------------| | `JulcEval.forClass(Class)` | Load source from `src/main/java` by class | | `JulcEval.forClass(Class, Path)` | Load source from a custom source root | | `JulcEval.forSource(String)` | Use inline Java source | **Mode 1: Interface proxy** — define an interface matching the on-chain methods, and call them with Java types: ```java interface MathProxy { BigInteger doubleIt(long x); boolean isPositive(long x); } var proxy = JulcEval.forClass(MathHelper.class) .create(MathProxy.class); assertEquals(BigInteger.valueOf(42), proxy.doubleIt(21)); assertTrue(proxy.isPositive(1)); ``` **Mode 2: Fluent `call()` API** — one-off calls with string method names: ```java var eval = JulcEval.forClass(MathHelper.class); assertEquals(BigInteger.valueOf(42), eval.call("doubleIt", 21).asInteger()); assertTrue(eval.call("isPositive", 1).asBoolean()); ``` **Supported argument types** (auto-converted to PlutusData): `BigInteger`, `int`, `long`, `boolean`, `byte[]`, `String`, `PlutusData`, `PlutusDataConvertible` **CallResult extraction methods:** | Method | Return type | |--------|------------| | `.asInteger()` | `BigInteger` | | `.asLong()` | `long` | | `.asInt()` | `int` | | `.asByteString()` | `byte[]` | | `.asBoolean()` | `boolean` | | `.asString()` | `String` | | `.asData()` | `PlutusData` | | `.asOptional()` | `Optional` | | `.asList()` | `List` | | `.as(Class)` | `T` (supports ledger types and primitives) | | `.auto()` | `Object` (auto-detected) | | `.rawTerm()` | `Term` (raw UPLC term) | **When to use which:** | Scenario | Use | |----------|-----| | Test a single helper method (math, string, logic) | `JulcEval` | | Test a full validator with datum + redeemer + ScriptContext | `ValidatorTest` | | End-to-end with budget checks and trace messages | `ValidatorTest` + `BudgetAssertions` | --- #### 15. Deploying ##### JulcScriptLoader Load pre-compiled scripts from the classpath (produced by the annotation processor): ```java import com.bloxbean.cardano.julc.clientlib.JulcScriptLoader; import com.bloxbean.cardano.client.plutus.spec.PlutusV3Script; // Non-parameterized PlutusV3Script script = JulcScriptLoader.load(VestingValidator.class); String hash = JulcScriptLoader.scriptHash(VestingValidator.class); // Parameterized — manual CCL PlutusData PlutusV3Script script = JulcScriptLoader.load(ParameterizedVesting.class, BytesPlutusData.of(ownerPkh), BigIntPlutusData.of(deadlineMs)); // Parameterized — using PlutusDataAdapter.convert() (simpler) PlutusV3Script script = JulcScriptLoader.load(ParameterizedVesting.class, PlutusDataAdapter.convert(ownerPkh), // byte[] → BytesPlutusData PlutusDataAdapter.convert(deadlineMs)); // BigInteger → BigIntPlutusData ``` ##### JulcScriptAdapter Convert a `Program` (from programmatic compilation) to a cardano-client-lib `PlutusV3Script`: ```java import com.bloxbean.cardano.julc.clientlib.JulcScriptAdapter; var program = compiler.compile(source).program(); PlutusV3Script script = JulcScriptAdapter.fromProgram(program); String hash = JulcScriptAdapter.scriptHash(program); ``` ##### cardano-client-lib integration Once you have a `PlutusV3Script`, use cardano-client-lib to build transactions: ```java import com.bloxbean.cardano.client.address.AddressProvider; import com.bloxbean.cardano.client.common.model.Networks; import com.bloxbean.cardano.client.quicktx.Tx; import com.bloxbean.cardano.client.quicktx.ScriptTx; import com.bloxbean.cardano.client.quicktx.QuickTxBuilder; import com.bloxbean.cardano.client.api.model.Amount; import com.bloxbean.cardano.client.function.helper.SignerProviders; // Derive the script address String scriptAddress = AddressProvider .getEntAddress(script, Networks.testnet()) .toBech32(); // Lock ADA to the script var lockTx = new Tx() .payToContract(scriptAddress, Amount.ada(5), datum) .from(account.baseAddress()); var result = quickTxBuilder.compose(lockTx) .withSigner(SignerProviders.signerFrom(account)) .completeAndWait(); // Unlock from the script var unlockTx = new ScriptTx() .collectFrom(scriptUtxo, redeemer) .payToAddress(account.baseAddress(), Amount.ada(4)) .attachSpendingValidator(script); var result = quickTxBuilder.compose(unlockTx) .withSigner(SignerProviders.signerFrom(account)) .feePayer(account.baseAddress()) .collateralPayer(account.baseAddress()) .completeAndWait(); // Mint tokens var asset = new Asset("MyToken", BigInteger.valueOf(100)); var mintTx = new ScriptTx() .mintAsset(script, asset, redeemer); var result = quickTxBuilder.compose(mintTx) .withSigner(SignerProviders.signerFrom(account)) .feePayer(account.baseAddress()) .collateralPayer(account.baseAddress()) .completeAndWait(); ``` ##### PlutusDataAdapter — Automatic Datum/Redeemer Conversion Instead of manually building CCL `ConstrPlutusData` for datums and redeemers, use `PlutusDataAdapter.convert()` to automatically convert your on-chain Java records and sealed interfaces to/from CCL `PlutusData`: ```java import com.bloxbean.cardano.julc.clientlib.PlutusDataAdapter; // Given your on-chain types: record AuctionDatum(byte[] seller, BigInteger deadline, BigInteger minBid) {} sealed interface Action permits Bid, Close {} record Bid(byte[] bidder, BigInteger amount) implements Action {} record Close() implements Action {} ``` **Java object → CCL PlutusData** (for building transactions): ```java // Record datum — no manual ConstrPlutusData construction needed var datum = PlutusDataAdapter.convert( new AuctionDatum(sellerPkh, BigInteger.valueOf(deadline), BigInteger.valueOf(minBid))); // Sealed interface redeemer — tag assigned from permits() order (Bid=0, Close=1) var redeemer = PlutusDataAdapter.convert( new Bid(bidderPkh, BigInteger.valueOf(7_000_000))); // Use directly with QuickTx var lockTx = new Tx() .payToContract(scriptAddress, Amount.ada(5), datum) .from(account.baseAddress()); ``` **CCL PlutusData → Java object** (when reading from chain): ```java // Decode a datum from a UTxO query result AuctionDatum datum = PlutusDataAdapter.convert(utxoPlutusData, AuctionDatum.class); // Decode a sealed interface — dispatches by ConstrData tag Action action = PlutusDataAdapter.convert(redeemerData, Action.class); // Returns Bid or Close depending on the tag ``` **Primitives** work directly for script parameters: ```java var paramBytes = PlutusDataAdapter.convert(new byte[]{0x01, 0x02, 0x03}); // → BytesPlutusData var paramInt = PlutusDataAdapter.convert(BigInteger.valueOf(42)); // → BigIntPlutusData var paramBool = PlutusDataAdapter.convert(true); // → ConstrPlutusData(1) var paramStr = PlutusDataAdapter.convert("TOKEN"); // → BytesPlutusData (UTF-8) ``` **Supported types:** | Java Type | PlutusData Encoding | |-----------|-------------------| | `BigInteger`, `int`, `long` | `IntData` | | `byte[]` | `BytesData` | | `boolean` | `ConstrData(0)` false / `ConstrData(1)` true | | `String` | `BytesData` (UTF-8 encoded) | | `Optional` | `ConstrData(0,[val])` present / `ConstrData(1,[])` empty | | `List`, `JulcList` | `ListData` | | `Map`, `JulcMap` | `MapData` | | `Tuple2`, `Tuple3` | `ConstrData(0, [fields...])` | | Record | `ConstrData(tag, [fields in declaration order])` | | Sealed interface variant | tag = position in `permits()` list | | `@NewType` record | Underlying type directly (no ConstrData wrap) | | Ledger types (`PubKeyHash`, etc.) | Handled via `toPlutusData()`/`fromPlutusData()` | The encoding rules match the JuLC compiler exactly, so on-chain and off-chain representations are binary-compatible. --- #### 16. Compiler Limitations The JuLC compiler supports a safe subset of Java for on-chain execution. The following limitations apply: ##### Variables and Assignment - **Immutable variables**: Variables cannot be reassigned after initialization. The only exception is loop accumulator variables in `while` and `for-each` loops. - **No uninitialized variables**: All variables must be initialized at declaration. ##### Types - **Supported types**: `BigInteger`, `boolean`, `byte[]`, `long`, `String`, `PlutusData`, records, sealed interfaces, `List`, `Map`, `Optional`, `Tuple2`, `Tuple3`, `JulcArray` *(PV11+)*. - **No float/double**: Floating-point types do not exist on-chain. - **No Java arrays** (except `byte[]`): Use `JulcList` for collections (or `List`), or `JulcArray` for O(1) random access on PV11+ networks (CIP-156). `byte[]` literal arrays (`new byte[]{0x48, 0x45}`) and `"TOKEN".getBytes()` are supported as compile-time constants. `JulcList` is preferred over `List` because it provides IDE autocomplete for on-chain methods (`.contains()`, `.size()`, `.get()`, `.filter()`, etc.). - **No class inheritance**: Only records and sealed interfaces are supported for data types. ##### Control Flow - **No `continue` statement**: Every branch in a loop body must assign all accumulator variables. - **No C-style `for(init; cond; step)`**: Use `while` or for-each. - **No `do-while`**: Use a `while` loop with an initial check. - **No `return` inside multi-accumulator loop body**: The loop must complete naturally. - **No `try`/`catch`/`throw`**: Errors are expressed via `Builtins.error()` which halts the CEK machine. - **No `null`**: There is no null concept on-chain. Use `Optional` where needed. - **No `this`/`super`**: All methods must be `static`. ##### Functions - **No lambda `.apply()`**: Lambda expressions can only be passed directly to HOF methods (like `ListsLib.map`). You cannot store a lambda in a variable and invoke it later. - **Multi-binding LetRec**: Mutual recursion is supported for up to 2 mutually recursive methods (via Bekic's theorem). More than 2 mutually recursive bindings are not supported. - **`map()` returns `JulcList`**: The `map` HOF wraps each lambda result to Data, so the returned list has `DataType` elements regardless of input. Use `Builtins.unIData()` or `Builtins.unBData()` to extract typed values from mapped results. ##### Type System Caveats - **`@Param` must use `PlutusData`**: Never use `PlutusData.BytesData`, `byte[]`, or other specific types for `@Param` fields. Param values are always raw Data at runtime. - **Cross-method type inference**: Calling a helper method with a `long` parameter from another method may generate `EqualsData` instead of `EqualsInteger`. Use Data-level equality as a workaround. - **Cross-library `BytesData` param bug**: When calling a stdlib method that takes `BytesData`-typed parameters from user code, if the caller has a `BytesData` variable of matching type, the compiler skips the needed conversion. Workaround: pass `PlutusData` (not `BytesData`) arguments to cross-library calls. - **Tuple2/Tuple3 not switchable**: These are registered as `RecordType`, not `SumType`. Use `.first()` and `.second()` field access instead of pattern matching. ##### Value and Ledger Types - **`Value.assetOf()` needs BData args**: Arguments must be wrapped with `Builtins.bData()` when passing `byte[]` to avoid `EqualsData` mismatches. - **Double `.hash()` on ledger hash types**: Types like `PubKeyHash`, `TxId`, `ScriptHash` map to `ByteStringType`. Calling `.hash()` extracts the raw ByteString. Calling `.hash()` again generates a second `UnBData` on an already- unwrapped value, which fails. Use `(byte[])(Object) pk.hash()` instead of `pk.hash().hash()`. ##### Switch Expressions - **`default` branch**: The `default ->` branch acts as a catch-all for uncovered variants. Prefer explicit cases for all variants of a sealed interface for clarity. The compiler checks exhaustiveness at compile time: if you omit a case and have no `default` branch, you get a compile error listing the missing variants. - **Field name shadows parameter**: In `case Variant f -> body`, the compiler binds the variant's field names in scope. If a method parameter has the same name as a field, the field binding shadows the parameter. Use different parameter names. ##### Not Supported - No standard Java library classes (only `BigInteger` and the JuLC stdlib) - No `new` for non-record classes - No generics beyond Tuple2/Tuple3 and built-in collections - No method references (`MyClass::method`) - No annotations beyond the JuLC-provided ones --- # Guides --- ## JuLC Advanced Guide: Low-Level Patterns Source: https://julc.dev/guides/advanced-guide/ > JuLC Advanced Guide: Low-Level Patterns - JuLC documentation This guide covers low-level programming patterns for developers who need to go beyond the typed API provided by JuLC's ledger types and stdlib libraries. It assumes familiarity with basic JuLC validator development (annotations, typed field access, stdlib usage) as covered in the getting-started guide. --- #### 1. Introduction The typed API (`ScriptContext`, `TxInfo`, `Value`, etc.) and stdlib libraries (`OutputLib`, `ValuesLib`, `ListsLib`, etc.) handle most smart contract needs. However, there are situations where you must drop down to low-level `Builtins` and raw `PlutusData` manipulation: - **Complex data manipulation** -- Building custom data structures, manually constructing Values, or working with nested maps/lists that the typed API does not cover. - **Performance optimization** -- Minimizing script budget by avoiding redundant field extractions, short-circuiting early, or inlining operations that a library call would make more expensive. - **Testing** -- Constructing mock `ScriptContext` and `TxInfo` as raw `PlutusData` for UPLC-level evaluation tests without using the full ledger record constructors. - **Working with raw PlutusData** -- Governance types, custom datum layouts, protocol-specific data formats, or any case where the typed API does not yet have a record definition. - **Cross-library interop** -- Understanding how compiled libraries receive and return Data at the UPLC boundary, and how to work around type-boundary mismatches. --- #### 2. PlutusData Encoding/Decoding ##### The Data Hierarchy All on-chain values are represented as `PlutusData`, a sealed interface with exactly five variants: | Variant | Java Type | On-Chain Representation | |---------|-----------|------------------------| | `ConstrData(tag, fields)` | `PlutusData.ConstrData` | Constructor application | | `IntData(value)` | `PlutusData.IntData` | Arbitrary-precision integer | | `BytesData(value)` | `PlutusData.BytesData` | Byte string | | `ListData(items)` | `PlutusData.ListData` | List of Data values | | `MapData(entries)` | `PlutusData.MapData` | Association list of key-value pairs | ##### Encoding Builtins (Java -> Data) These `Builtins` methods wrap primitive values into their Data representation: ```java import com.bloxbean.cardano.julc.stdlib.Builtins; // Wrap an integer as IntData PlutusData wrapped = Builtins.iData(42); PlutusData wrappedBig = Builtins.iData(BigInteger.valueOf(1000000)); // Wrap a byte array as BytesData PlutusData wrappedBytes = Builtins.bData(new byte[]{0x01, 0x02, 0x03}); // Construct a ConstrData from tag + fields list PlutusData.ListData fields = Builtins.mkCons(Builtins.iData(1), Builtins.mkNilData()); PlutusData.ConstrData constr = Builtins.constrData(0, fields); // Wrap a ListData PlutusData.ListData listWrapped = Builtins.listData(someList); // Wrap a MapData PlutusData.MapData mapWrapped = Builtins.mapData(somePairList); ``` ##### Decoding Builtins (Data -> Java) These `Builtins` methods extract primitive values from their Data wrappers: ```java // Extract integer from IntData BigInteger n = Builtins.unIData(someData); // throws if not IntData // Extract BytesData from Data PlutusData.BytesData bs = Builtins.unBData(someData); // throws if not BytesData // Deconstruct ConstrData into (tag, fields) pair PlutusData.ConstrData pair = Builtins.unConstrData(someData); // pair is ConstrData(0, [IntData(tag), ListData(fields)]) long tag = Builtins.constrTag(someData); // shortcut: extract tag PlutusData.ListData flds = Builtins.constrFields(someData); // shortcut: extract fields // Extract ListData PlutusData.ListData ld = Builtins.unListData(someData); // Extract MapData PlutusData.MapData md = Builtins.unMapData(someData); ``` ##### Record Encoding Records (product types) are encoded as `ConstrData` with tag 0 and fields in declaration order: ``` record Datum(BigInteger deadline, byte[] beneficiary) -> Constr(0, [IntData(deadline), BytesData(beneficiary)]) ``` ##### Sealed Interface (Sum Type) Encoding Sealed interface variants use ascending constructor tags starting from 0: ``` sealed interface Credential { record PubKeyCredential(PubKeyHash hash) ... // tag 0 record ScriptCredential(ScriptHash hash) ... // tag 1 } PubKeyCredential(pkh) -> Constr(0, [BytesData(pkh)]) ScriptCredential(sh) -> Constr(1, [BytesData(sh)]) ``` ##### Boolean Encoding Booleans are encoded as constructors with no fields: ``` true -> Constr(1, []) // tag 1 = True false -> Constr(0, []) // tag 0 = False ``` ##### Optional Encoding Optional values follow Haskell convention: ``` Some(x) -> Constr(0, [x]) // tag 0 = Just None -> Constr(1, []) // tag 1 = Nothing ``` Example -- extracting an Optional datum: ```java @SpendingValidator class OptionalExample { @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { // Get the optional datum from ScriptInfo PlutusData optDatum = (PlutusData)(Object) ctx.scriptInfo(); var scriptFields = Builtins.constrFields(optDatum); // SpendingScript is tag 1: Constr(1, [txOutRef, optionalDatum]) var optField = Builtins.headList(Builtins.tailList(scriptFields)); // Check if Some (tag 0) or None (tag 1) if (Builtins.constrTag(optField) == 0) { // Some: extract the datum value PlutusData datum = Builtins.headList(Builtins.constrFields(optField)); return Builtins.unIData(datum).compareTo(BigInteger.ZERO) > 0; } else { // None: no datum provided return false; } } } ``` --- #### 3. Type Casting Patterns JuLC compiles all types down to `Data` at the UPLC level. Casts in Java source are no-ops on-chain but serve as type hints for the compiler. ##### Cast PlutusData to a Ledger Type When you have raw `PlutusData` and need to treat it as a ledger type: ```java // Recommended: PlutusData.cast() PlutusData rawData = Builtins.headList(someList); TxOut txOut = PlutusData.cast(rawData, TxOut.class); // Also works: double-cast TxOut txOut2 = (TxOut)(Object) rawData; // Hash types PlutusData rawHash = Builtins.headList(credentialFields); PubKeyHash pkh = PlutusData.cast(rawHash, PubKeyHash.class); ``` ##### Extract Raw Hash Bytes Ledger hash types (`PubKeyHash`, `TxId`, `ScriptHash`, `DatumHash`, `PolicyId`, `TokenName`, `ValidatorHash`) map to `ByteStringType` on-chain. Calling `.hash()` already extracts the raw `ByteString` via `UnBData(HeadList(...))`. ```java // CORRECT: single .hash() extracts the byte[] byte[] rawBytes = (byte[])(Object) pk.hash(); // WRONG: double .hash() applies UnBData on an already-unwrapped ByteString // byte[] broken = pk.hash().hash(); // Runtime error: UnBData(ByteString) // For TxId, same pattern applies // CORRECT: byte[] txIdBytes = (byte[])(Object) ref.txId(); // WRONG: // byte[] broken = ref.txId().hash(); // Runtime error ``` ##### When to Use Type.of() vs Casts The `Type.of(byte[])` factory methods are for creating ledger types from raw `byte[]` values: ```java // Use Type.of() when you have byte[] and want a ledger type byte[] hashBytes = Builtins.sha2_256(someData); PubKeyHash pkh = PubKeyHash.of(hashBytes); PolicyId pid = PolicyId.of(policyBytes); // Use PlutusData.cast() when you have PlutusData and want a ledger type PlutusData rawFromList = Builtins.headList(signatories); PubKeyHash pkh2 = PlutusData.cast(rawFromList, PubKeyHash.class); // Also works: double-cast PubKeyHash pkh3 = (PubKeyHash)(Object) rawFromList; ``` Key rule: `Type.of(byte[])` is for `byte[]` arguments. `PlutusData.cast()` or double-casts are for `PlutusData` arguments. `PlutusData.cast()` is preferred for readability. ##### PlutusData.cast() — Clean Type Casting Instead of the double-cast pattern, use `PlutusData.cast()`: ```java // Old pattern (still works) MyDatum datum = (MyDatum)(Object) rawData; // New pattern (recommended) MyDatum datum = PlutusData.cast(rawData, MyDatum.class); ``` Works with all target types — records, sealed interfaces, ledger types, `JulcMap`, `byte[]`, hash types: ```java // Cast to custom record var datum = PlutusData.cast(datumData, AuctionDatum.class); // Cast to sealed interface (use in switch) var action = PlutusData.cast(redeemer, Action.class); return switch (action) { case Mint m -> m.amount() > 0; case Burn b -> b.amount() > 0; }; // Cast to ledger type var val = PlutusData.cast(rawValue, Value.class); // Cast hash types byte[] policyBytes = PlutusData.cast(mintInfo.policyId(), byte[].class); ScriptHash sh = PlutusData.cast(policyBytes, ScriptHash.class); // Chained field access boolean ok = PlutusData.cast(redeemer, MyDatum.class).amount() == 42; ``` On-chain: zero cost (compiles to identity, same as the double-cast). Off-chain: unchecked cast at JVM level. **Note:** The second argument must be a literal `ClassName.class` expression. A variable holding a `Class` is not supported on-chain. ###### Generic Collections: JulcList and JulcMap Java class literals cannot carry generic type parameters (`JulcList.class` not `JulcList.class`). When casting to generic collections, use an **explicit type declaration** on the left side — the compiler reads the generic info from the declared type, not from the `PlutusData.cast()` return: ```java // CORRECT: explicit type preserves generics — element type is MyRecord JulcList records = PlutusData.cast(data, JulcList.class); // CORRECT: explicit type preserves key/value types JulcMap lookup = PlutusData.cast(data, JulcMap.class); // AVOID: var loses generic info — element type defaults to DataType var records = PlutusData.cast(data, JulcList.class); // JulcList ``` This works because the JuLC compiler resolves the variable type from the declared type (`JulcList`) rather than inferring it from the right-hand side. The generic parameters give the compiler the element/key/value types needed for typed access on list and map elements. For nested generics, the same rule applies: ```java // Map with typed values JulcMap> nested = PlutusData.cast(data, JulcMap.class); ``` ##### The Double .hash() Bug Explained The `.hash()` accessor on hash types generates UPLC code that does `UnBData(HeadList(fields))`. The result is already a raw `ByteString`. Calling `.hash()` again generates `UnBData(ByteString)` which crashes at runtime with a `DeserializationError` because `ByteString` is not `BytesData`. ```java @SpendingValidator class HashExample { @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); PubKeyHash signer = (PubKeyHash)(Object) Builtins.headList( (PlutusData)(Object) txInfo.signatories() ); // CORRECT: extract raw bytes with cast byte[] signerBytes = (byte[])(Object) signer.hash(); // Use the raw bytes for comparison byte[] expected = new byte[28]; return Builtins.equalsByteString(signerBytes, expected); } } ``` --- #### 4. Low-Level List Manipulation When the typed `JulcList` or `ListsLib` API is not sufficient, you can build and traverse lists using raw `Builtins`. ##### Building Lists Lists are built from the end by prepending elements onto an empty list: ```java @SpendingValidator class ListBuilder { @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { // Build a list [3, 2, 1] by prepending in reverse order PlutusData.ListData list = Builtins.mkNilData(); list = Builtins.mkCons(Builtins.iData(1), list); list = Builtins.mkCons(Builtins.iData(2), list); list = Builtins.mkCons(Builtins.iData(3), list); // list is now [3, 2, 1] BigInteger first = Builtins.unIData(Builtins.headList(list)); return first.equals(BigInteger.valueOf(3)); } } ``` ##### Manual List Traversal The standard pattern for traversing a list uses `nullList`, `headList`, and `tailList`: ```java @SpendingValidator class ListTraversal { // Sum all integers in a list static BigInteger sumList(PlutusData list) { BigInteger total = BigInteger.ZERO; PlutusData cursor = list; while (!Builtins.nullList(cursor)) { var item = Builtins.headList(cursor); total = total.add(Builtins.unIData(item)); cursor = Builtins.tailList(cursor); } return total; } @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { PlutusData.ListData nums = Builtins.mkNilData(); nums = Builtins.mkCons(Builtins.iData(10), nums); nums = Builtins.mkCons(Builtins.iData(20), nums); nums = Builtins.mkCons(Builtins.iData(30), nums); BigInteger sum = sumList(nums); return sum.equals(BigInteger.valueOf(60)); } } ``` ##### Pair List Typing: mkNilPairData vs mkNilData When building lists of pairs (for map construction), you **must** use `Builtins.mkNilPairData()` instead of `Builtins.mkNilData()`. The Scalus VM strictly checks element types, and a list created with `mkNilData` is typed as `List[Data]`, not `List[Pair[Data, Data]]`. ```java // CORRECT: use mkNilPairData for pair lists PlutusData pairList = Builtins.mkNilPairData(); var pair = Builtins.mkPairData(Builtins.bData(key), Builtins.iData(42)); pairList = Builtins.mkCons(pair, pairList); // WRONG: using mkNilData for pair lists // PlutusData pairList = Builtins.mkNilData(); // VM type error! ``` --- #### 5. Map Construction and Pair Manipulation Maps on-chain are association lists of pairs: `List[Pair[Data, Data]]`. ##### Building Maps from Pairs ```java @SpendingValidator class MapBuilder { @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { // Build a map { "alice" -> 100, "bob" -> 200 } PlutusData emptyPairList = Builtins.mkNilPairData(); var bobPair = Builtins.mkPairData( Builtins.bData("bob".getBytes()), Builtins.iData(200) ); var alicePair = Builtins.mkPairData( Builtins.bData("alice".getBytes()), Builtins.iData(100) ); PlutusData myMap = Builtins.mkCons(alicePair, Builtins.mkCons(bobPair, emptyPairList)); // Wrap as MapData PlutusData.MapData mapData = Builtins.mapData(myMap); // Lookup "alice": traverse the pair list BigInteger aliceAmount = lookupAmount( Builtins.unMapData(mapData), Builtins.bData("alice".getBytes())); return aliceAmount.equals(BigInteger.valueOf(100)); } static BigInteger lookupAmount(PlutusData pairs, PlutusData key) { BigInteger result = BigInteger.ZERO; PlutusData cursor = pairs; while (!Builtins.nullList(cursor)) { var pair = Builtins.headList(cursor); if (Builtins.equalsData(Builtins.fstPair(pair), key)) { result = Builtins.unIData(Builtins.sndPair(pair)); cursor = Builtins.mkNilPairData(); // break } else { cursor = Builtins.tailList(cursor); } } return result; } } ``` ##### Traversing Maps To traverse a map, first unwrap it with `unMapData` to get the pair list, then iterate using `fstPair`/`sndPair` on each element: ```java @SpendingValidator class MapTraversal { // Count entries in a map static long countEntries(PlutusData mapValue) { var pairs = Builtins.unMapData(mapValue); long count = 0; PlutusData cursor = pairs; while (!Builtins.nullList(cursor)) { var pair = Builtins.headList(cursor); PlutusData key = Builtins.fstPair(pair); PlutusData value = Builtins.sndPair(pair); // Process key/value... count = count + 1; cursor = Builtins.tailList(cursor); } return count; } @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); PlutusData withdrawals = (PlutusData)(Object) txInfo.withdrawals(); return countEntries(withdrawals) > 0; } } ``` ##### MapType Always Holds Pair Lists Internally, JuLC represents `MapType` variables as pair lists (not wrapped `MapData`). This means: - Field access like `txInfo.withdrawals()` already returns a pair list. - Do NOT call `Builtins.unMapData()` on the result of a field access that the compiler already knows is a map -- this would double-unwrap. - When you receive a `PlutusData.MapData` from an external source (e.g., a datum or parameter), you DO need `Builtins.unMapData()` to get the pair list. ##### For-Each on Maps The `for (var entry : map)` syntax on a `MapType` variable automatically prepends `UnMapData` and yields `PairType` elements: ```java @SpendingValidator class MapForEach { @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); var withdrawals = txInfo.withdrawals(); BigInteger totalWithdrawn = BigInteger.ZERO; for (var entry : withdrawals) { // entry is PairType: entry.key() gives Credential, entry.value() gives amount BigInteger amount = (BigInteger)(Object) entry.value(); totalWithdrawn = totalWithdrawn.add(amount); } return totalWithdrawn.compareTo(BigInteger.ZERO) > 0; } } ``` --- #### 6. Raw Value Manipulation ##### Value Structure A Cardano `Value` is a nested map: ``` Map> ^ ^ ^ | | | policy ID token name quantity ``` Lovelace (ADA) is stored under the **empty bytestring** policy ID and the **empty bytestring** token name. ##### Manual Lovelace Extraction This is what `ValuesLib.lovelaceOf()` does under the hood: ```java @SpendingValidator class LovelaceExtraction { static BigInteger extractLovelace(PlutusData value) { // Value is MapData: first entry is the ADA policy (empty ByteString) var outerPairs = Builtins.unMapData(value); var firstOuterPair = Builtins.headList(outerPairs); // The value of the first pair is the inner token map var innerMap = Builtins.sndPair(firstOuterPair); var innerPairs = Builtins.unMapData(innerMap); var firstInnerPair = Builtins.headList(innerPairs); // The value of the first inner pair is the lovelace amount return Builtins.unIData(Builtins.sndPair(firstInnerPair)); } @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); var outputs = txInfo.outputs(); TxOut firstOutput = (TxOut)(Object) Builtins.headList( (PlutusData)(Object) outputs); PlutusData outputValue = (PlutusData)(Object) firstOutput.value(); BigInteger lovelace = extractLovelace(outputValue); return lovelace.compareTo(BigInteger.valueOf(2_000_000)) >= 0; } } ``` ##### Manual Asset Lookup Looking up a specific native token amount requires traversing both the outer map (by policy ID) and the inner map (by token name): ```java @SpendingValidator class AssetLookup { static BigInteger findAssetAmount(PlutusData value, PlutusData policyId, PlutusData tokenName) { var outerPairs = Builtins.unMapData(value); BigInteger result = BigInteger.ZERO; PlutusData outerCursor = outerPairs; // Search outer map for matching policy while (!Builtins.nullList(outerCursor)) { var outerPair = Builtins.headList(outerCursor); if (Builtins.equalsData(Builtins.fstPair(outerPair), policyId)) { // Found policy, search inner map for token name var innerPairs = Builtins.unMapData(Builtins.sndPair(outerPair)); PlutusData innerCursor = innerPairs; while (!Builtins.nullList(innerCursor)) { var innerPair = Builtins.headList(innerCursor); if (Builtins.equalsData(Builtins.fstPair(innerPair), tokenName)) { result = Builtins.unIData(Builtins.sndPair(innerPair)); innerCursor = Builtins.mkNilPairData(); // break } else { innerCursor = Builtins.tailList(innerCursor); } } outerCursor = Builtins.mkNilPairData(); // break } else { outerCursor = Builtins.tailList(outerCursor); } } return result; } @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); PlutusData mintValue = (PlutusData)(Object) txInfo.mint(); byte[] myPolicy = new byte[28]; // your policy ID byte[] myToken = "MyToken".getBytes(); BigInteger minted = findAssetAmount(mintValue, Builtins.bData(myPolicy), Builtins.bData(myToken)); return minted.equals(BigInteger.ONE); } } ``` ##### Building a Value Manually ```java // Build a Value containing 5 ADA + 1 MyToken static PlutusData buildValue(byte[] policyId, byte[] tokenName) { var emptyPairList = Builtins.mkNilPairData(); var emptyBs = Builtins.bData(new byte[0]); // Inner map for ADA: { "" -> 5000000 } var adaTokenPair = Builtins.mkPairData(emptyBs, Builtins.iData(5_000_000)); var adaInnerMap = Builtins.mapData( Builtins.mkCons(adaTokenPair, emptyPairList)); // Inner map for native token: { tokenName -> 1 } var tokenPair = Builtins.mkPairData( Builtins.bData(tokenName), Builtins.iData(1)); var tokenInnerMap = Builtins.mapData( Builtins.mkCons(tokenPair, emptyPairList)); // Outer map: { "" -> adaInnerMap, policyId -> tokenInnerMap } var adaOuterPair = Builtins.mkPairData(emptyBs, adaInnerMap); var tokenOuterPair = Builtins.mkPairData( Builtins.bData(policyId), tokenInnerMap); var outerList = Builtins.mkCons(adaOuterPair, Builtins.mkCons(tokenOuterPair, emptyPairList)); return Builtins.mapData(outerList); } ``` --- #### 7. Working with Raw PlutusData ##### When Typed Access Is Not Enough The typed API covers most standard Plutus V3 types, but you may need raw `PlutusData` for: - **Governance types** that are not fully modeled yet. - **Custom datum layouts** from other smart contract protocols. - **Test fixtures** where you want to build ScriptContext directly as Data. - **Generic validators** that must handle arbitrary datum/redeemer shapes. ##### Building ScriptContext for Tests The `PlutusData` interface provides convenience factory methods: ```java import com.bloxbean.cardano.julc.core.PlutusData; // V3 ScriptContext structure: Constr(0, [txInfo, redeemer, scriptInfo]) PlutusData scriptContext = PlutusData.constr(0, buildTxInfo(), PlutusData.integer(42), // redeemer buildScriptInfo() ); PlutusData buildScriptInfo() { // SpendingScript = Constr(1, [txOutRef, optionalDatum]) PlutusData txOutRef = PlutusData.constr(0, PlutusData.constr(0, PlutusData.bytes(new byte[32])), // TxId PlutusData.integer(0) // index ); PlutusData noDatum = PlutusData.constr(1); // None return PlutusData.constr(1, txOutRef, noDatum); } ``` ##### V3 ScriptContext Structure ``` ScriptContext = Constr(0, [txInfo, redeemer, scriptInfo]) ``` ##### TxInfo Structure (16 fields, all under Constr tag 0) ``` TxInfo = Constr(0, [ inputs, // 0: List[TxInInfo] referenceInputs, // 1: List[TxInInfo] outputs, // 2: List[TxOut] fee, // 3: Integer (lovelace) mint, // 4: Value (Map) certificates, // 5: List[TxCert] withdrawals, // 6: Map[Credential, Integer] validRange, // 7: Interval signatories, // 8: List[PubKeyHash (ByteString)] redeemers, // 9: Map[ScriptPurpose, Data] datums, // 10: Map[DatumHash, Data] txId, // 11: TxId = Constr(0, [ByteString]) votes, // 12: Map[Voter, Map[GovernanceActionId, Vote]] proposalProcedures, // 13: List[ProposalProcedure] currentTreasuryAmount, // 14: Optional Integer treasuryDonation // 15: Optional Integer ]) ``` ##### Building a Minimal TxInfo ```java PlutusData buildMinimalTxInfo(byte[] signerPkh) { PlutusData emptyList = PlutusData.list(); PlutusData emptyMap = PlutusData.map(); byte[] emptyBs = new byte[0]; // Interval: always valid = (NegInf inclusive, PosInf inclusive) PlutusData negInf = PlutusData.constr(0, // NegInf = tag 0 PlutusData.constr(0)); // inclusive = true (Constr 1 [] would be false) PlutusData posInf = PlutusData.constr(0, PlutusData.constr(2)); // PosInf = tag 2 // Actually, IntervalBound = Constr(0, [boundType, isInclusive]) // where isInclusive is Boolean: Constr(1,[])=True, Constr(0,[])=False PlutusData trueVal = PlutusData.constr(1); PlutusData lowerBound = PlutusData.constr(0, PlutusData.constr(0), // NegInf trueVal); PlutusData upperBound = PlutusData.constr(0, PlutusData.constr(2), // PosInf trueVal); PlutusData validRange = PlutusData.constr(0, lowerBound, upperBound); // Signatories: list of PubKeyHash (just ByteStrings) PlutusData signatories = PlutusData.list(PlutusData.bytes(signerPkh)); // Optional None for treasury fields PlutusData none = PlutusData.constr(1); // TxId PlutusData txId = PlutusData.constr(0, PlutusData.bytes(new byte[32])); return PlutusData.constr(0, emptyList, // inputs emptyList, // referenceInputs emptyList, // outputs PlutusData.integer(200_000), // fee emptyMap, // mint emptyList, // certificates emptyMap, // withdrawals validRange, // validRange signatories, // signatories emptyMap, // redeemers emptyMap, // datums txId, // txId emptyMap, // votes emptyList, // proposalProcedures none, // currentTreasuryAmount none // treasuryDonation ); } ``` ##### Extracting Fields by Position When you cannot use the typed API, you can extract fields by position: ```java @SpendingValidator class RawFieldAccess { @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { // Access ScriptContext fields by position (0=txInfo, 1=redeemer, 2=scriptInfo) PlutusData rawCtx = (PlutusData)(Object) ctx; var ctxFields = Builtins.constrFields(rawCtx); PlutusData txInfo = Builtins.headList(ctxFields); // Access TxInfo field 8 (signatories) by chaining tailList var txFields = Builtins.constrFields(txInfo); PlutusData signatories = txFields; // Skip first 8 fields long idx = 8; PlutusData cursor = txFields; while (idx > 0) { cursor = Builtins.tailList(cursor); idx = idx - 1; } PlutusData sigs = Builtins.headList(cursor); // Check at least one signatory return !Builtins.nullList(sigs); } } ``` --- #### 8. Debugging and Tracing ##### Builtins.trace The `Builtins.trace` method emits a trace message and returns the second argument unchanged. This is compiled to the UPLC `Trace` builtin: ```java @SpendingValidator class TracingValidator { @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); BigInteger fee = txInfo.fee(); // Trace a message and continue Builtins.trace("Checking fee amount", PlutusData.UNIT); if (fee.compareTo(BigInteger.valueOf(1_000_000)) > 0) { Builtins.trace("Fee is above threshold", PlutusData.UNIT); return true; } else { Builtins.trace("Fee too low, rejecting", PlutusData.UNIT); return false; } } } ``` ##### ContextsLib.trace A shorter form that does not require a return value argument: ```java @SpendingValidator class ShortTrace { @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { ContextsLib.trace("Validator entered"); // ... validation logic return true; } } ``` ##### Collecting Traces in Tests When evaluating a compiled program, traces are collected in the `EvalResult`: ```java var program = ValidatorTest.compile(validatorSource); var result = ValidatorTest.evaluate(validatorSource, scriptContext); // Access traces if (result instanceof EvalResult.Success success) { List traces = success.traces(); for (String trace : traces) { System.out.println("TRACE: " + trace); } } if (result instanceof EvalResult.Failure failure) { List traces = failure.traces(); // Traces emitted before the failure point are still collected } ``` ##### Common Runtime Errors | Error Message | Cause | Fix | |---|---|---| | `DeserializationError` | Type mismatch -- applying `UnIData` to non-integer, `UnBData` to non-bytes, etc. | Check that your Data value actually has the expected type. Often caused by double `.hash()` or missing `wrapEncode`/`wrapDecode`. | | `NonPositiveInteger` | Passing zero or negative values to crypto operations that require positive integers. | Validate inputs before passing to crypto builtins. | | `BudgetExhausted` | Script exceeded CPU or memory budget. | Optimize with techniques from Section 9. | | `headList: empty list` | Calling `headList` on an empty list. | Check `!nullList(cursor)` before accessing head. | | `Expected ListData, got ...` | Passing a non-list value to a list operation. | Ensure the value is properly wrapped as `ListData`. | --- #### 9. Budget Measurement and Optimization ##### Understanding Per-Operation Costs Approximate CPU costs for common operations: | Operation | CPU Cost (approx.) | |---|---| | Field access (`HeadList`, `TailList`) | ~5,000 | | Arithmetic (`AddInteger`, `MultiplyInteger`) | ~10,000 | | Comparison (`EqualsInteger`, `LessThanInteger`) | ~10,000 | | Data equality (`EqualsData`) | ~50,000+ (depends on structure size) | | Crypto (`Sha2_256`, `Blake2b_256`) | ~100,000+ | | Signature verification (`VerifyEd25519Signature`) | ~5,000,000+ | ##### Fail-Fast with Builtins.error() Exit immediately when a validation check fails, saving budget on the remaining operations: ```java @SpendingValidator class FailFast { @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); // Check cheapest condition first if (txInfo.fee().compareTo(BigInteger.ZERO) <= 0) { Builtins.error(); } // More expensive check: only reached if fee check passes if (!ContextsLib.signedBy(txInfo, new byte[28])) { Builtins.error(); } return true; } } ``` ##### Cache Repeated Field Access Each field access generates `HeadList(TailList(...))` chains. Extract fields once and reuse the local variable: ```java @SpendingValidator class CachedAccess { @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { // BAD: accesses txInfo.outputs() three times, each time // re-extracting from the Constr fields // long count1 = OutputLib.countOutputsAt(ctx.txInfo().outputs(), addr); // long count2 = OutputLib.countOutputsAt(ctx.txInfo().outputs(), addr2); // var outs = ctx.txInfo().outputs(); // GOOD: extract once, reuse TxInfo txInfo = ctx.txInfo(); var outputs = txInfo.outputs(); var signatories = txInfo.signatories(); // Now use the cached variables return !Builtins.nullList((PlutusData)(Object) outputs) && !Builtins.nullList((PlutusData)(Object) signatories); } } ``` ##### Short-Circuit: Check Cheapest Conditions First Order your validation checks from cheapest to most expensive: ```java @SpendingValidator class ShortCircuit { @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); // 1. Integer comparison (~10K CPU) -- cheapest if (txInfo.fee().compareTo(BigInteger.valueOf(200_000)) < 0) { return false; } // 2. Data equality (~50K CPU) -- moderate byte[] expectedSigner = new byte[28]; if (!ContextsLib.signedBy(txInfo, expectedSigner)) { return false; } // 3. Crypto verification (~5M CPU) -- most expensive, checked last // Only reached if both cheap checks pass return true; } } ``` ##### Script Size: Avoid Unused Methods Every helper method in your validator class gets compiled into the UPLC output, even if it is not called. Remove unused methods to reduce script size. Prefer using stdlib library methods (`ListsLib`, `ValuesLib`, etc.) over reimplementing common operations inline -- the compiled library code is shared and the compiler avoids duplicating it. --- #### 10. Cross-Library Call Patterns ##### Data Boundary Semantics When your validator calls a method from an `@OnchainLibrary` class, the compiled library expects **raw Data** values at the UPLC boundary. The compiler normally handles the necessary encode/decode conversions, but there are edge cases. ##### The BytesData Parameter Bug If the caller has a variable typed as `BytesData` (or `MapData`, etc.) and the library method also has a parameter typed as `BytesData`, the compiler sees matching types and **skips the conversion**. But the compiled library expects raw `Data` at the UPLC boundary, not already-unwrapped `ByteString`. ```java @SpendingValidator class CrossLibraryBug { @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { // BAD: policyId is typed as byte[] (ByteStringType) // Calling ValuesLib.containsBytes() with a ByteStringType arg // may skip the necessary bData() wrapping byte[] policyId = new byte[28]; // ListsLib.containsBytes(list, policyId) -- potential type mismatch! // GOOD: use PlutusData typed variable PlutusData policyIdData = Builtins.bData(policyId); // Now the compiler sees PlutusData (DataType) and passes it through correctly return true; } } ``` ##### Workaround: Use PlutusData Variables When calling stdlib methods that take typed params (`BytesData`, `MapData`, etc.), declare your variables as `PlutusData` instead: ```java @SpendingValidator class SafeCrossLibrary { @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); // Use PlutusData instead of byte[] for cross-library calls PlutusData policyId = Builtins.bData(new byte[28]); PlutusData tokenName = Builtins.bData(new byte[0]); // ValuesLib.assetOf expects Data at the boundary -- PlutusData passes through safely BigInteger amount = ValuesLib._assetOf( txInfo.mint(), (PlutusData.BytesData) policyId, (PlutusData.BytesData) tokenName ); return amount.compareTo(BigInteger.ZERO) > 0; } } ``` ##### @Param Must Be PlutusData The `@Param` annotation marks fields that are baked into the script at deploy time. `@Param` values are **always raw Data at runtime**, regardless of their declared type. ```java @SpendingValidator class ParameterizedValidator { // CORRECT: always use PlutusData for @Param @Param PlutusData ownerPkh; @Param PlutusData minAmount; @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { BigInteger amount = Builtins.unIData(minAmount); return amount.compareTo(BigInteger.ZERO) > 0; } } // WRONG: using typed params causes issues // @Param PlutusData.BytesData ownerPkh; // broken: double-wraps with bData() // @Param byte[] tokenPolicy; // broken: type mismatch at runtime ``` ##### Local Wrapper Method Pattern If you need to call a stdlib method with typed params frequently, create a local wrapper in your validator: ```java @SpendingValidator class WrapperPattern { // Local wrapper avoids cross-library type boundary issues static BigInteger getAssetAmount(Value value, PlutusData policy, PlutusData token) { // Extract the amount manually, avoiding the cross-library boundary var outerPairs = Builtins.unMapData(value); BigInteger result = BigInteger.ZERO; PlutusData cursor = outerPairs; while (!Builtins.nullList(cursor)) { var pair = Builtins.headList(cursor); if (Builtins.equalsData(Builtins.fstPair(pair), policy)) { var innerPairs = Builtins.unMapData(Builtins.sndPair(pair)); result = ValuesLib.findTokenAmount( (PlutusData.MapData) Builtins.sndPair(pair), (PlutusData.BytesData) token); cursor = Builtins.mkNilPairData(); } else { cursor = Builtins.tailList(cursor); } } return result; } @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { PlutusData policy = Builtins.bData(new byte[28]); PlutusData token = Builtins.bData("Token".getBytes()); BigInteger minted = getAssetAmount(ctx.txInfo().mint(), policy, token); return minted.equals(BigInteger.ONE); } } ``` --- #### 11. Complex Sealed Interface Hierarchies ##### Multi-Level Nesting Sealed interfaces can be nested. For example, `ScriptInfo` has six variants, and `Credential` has two variants: ```java @SpendingValidator class NestedSwitch { @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { // First-level switch: ScriptInfo variants return switch (ctx.scriptInfo()) { case ScriptInfo.SpendingScript ss -> handleSpending(ctx, ss); case ScriptInfo.MintingScript ms -> false; case ScriptInfo.RewardingScript rs -> false; case ScriptInfo.CertifyingScript cs -> false; case ScriptInfo.VotingScript vs -> false; case ScriptInfo.ProposingScript ps -> false; }; } static boolean handleSpending(ScriptContext ctx, ScriptInfo.SpendingScript ss) { TxInfo txInfo = ctx.txInfo(); TxOut firstOutput = (TxOut)(Object) Builtins.headList( (PlutusData)(Object) txInfo.outputs()); // Second-level switch: Credential variants within Address return switch (firstOutput.address().credential()) { case Credential.PubKeyCredential pk -> { byte[] hash = (byte[])(Object) pk.hash(); yield Builtins.lengthOfByteString(hash) == 28; } case Credential.ScriptCredential sc -> { byte[] hash = (byte[])(Object) sc.hash(); yield Builtins.lengthOfByteString(hash) == 28; } }; } } ``` ##### Switch Best Practices **Exhaust all cases explicitly.** The `default` branch works as a catch-all for uncovered variants, but prefer listing every variant explicitly for clarity: ```java // OK but not recommended: default catches all unlisted variants return switch (ctx.scriptInfo()) { case ScriptInfo.SpendingScript ss -> true; default -> false; // compiled — covers all other ScriptInfo variants }; // RECOMMENDED: list all cases explicitly return switch (ctx.scriptInfo()) { case ScriptInfo.SpendingScript ss -> true; case ScriptInfo.MintingScript ms -> false; case ScriptInfo.RewardingScript rs -> false; case ScriptInfo.CertifyingScript cs -> false; case ScriptInfo.VotingScript vs -> false; case ScriptInfo.ProposingScript ps -> false; }; ``` The compiler enforces exhaustiveness: if you omit a case (and have no `default`), you will get a compile error listing the missing cases. ##### Field Name Collision When a switch case destructures a variant, the compiler binds the constructor's field names in scope. If a method parameter has the same name as a field, the field binding **shadows** the parameter. ```java // WRONG: parameter "time" is shadowed by Finite's field "time" static boolean checkBound(IntervalBound bound, BigInteger time) { return switch (bound.boundType()) { case IntervalBoundType.Finite f -> // f.time() returns the Finite field, but "time" (the parameter) // is also bound to f.time() due to shadowing! // This comparison always returns true (self-comparison). time.compareTo(f.time()) >= 0; case IntervalBoundType.NegInf ignored -> true; case IntervalBoundType.PosInf ignored -> false; }; } // CORRECT: use a different parameter name static boolean checkBound(IntervalBound bound, BigInteger point) { return switch (bound.boundType()) { case IntervalBoundType.Finite f -> point.compareTo(f.time()) >= 0; // "point" != "time", no shadowing case IntervalBoundType.NegInf ignored -> true; case IntervalBoundType.PosInf ignored -> false; }; } ``` This applies to all sealed interface switch cases. The rule: **never name a method parameter the same as any field in the variants you are matching on.** --- #### 12. Recursion Patterns ##### Automatic Z-Combinator Wrapping Helper methods in JuLC validators automatically support self-recursion. The compiler wraps recursive helper methods with the Z-combinator at the UPLC level, so you can write natural recursive code: ```java @SpendingValidator class RecursiveValidator { // Recursive factorial -- compiled with Z-combinator static BigInteger factorial(BigInteger n) { if (n.equals(BigInteger.ZERO)) { return BigInteger.ONE; } else { return n.multiply(factorial(n.subtract(BigInteger.ONE))); } } @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { BigInteger n = Builtins.unIData(redeemer); BigInteger result = factorial(n); return result.equals(BigInteger.valueOf(120)); // 5! = 120 } } ``` ##### List Traversal with Recursion Recursive list operations are a natural fit: ```java @SpendingValidator class RecursiveList { // Count elements in a list recursively static long countElements(PlutusData list) { if (Builtins.nullList(list)) { return 0; } else { return 1 + countElements(Builtins.tailList(list)); } } // Check if any element satisfies a condition (value > threshold) static boolean anyGreaterThan(PlutusData list, BigInteger threshold) { if (Builtins.nullList(list)) { return false; } else { BigInteger head = Builtins.unIData(Builtins.headList(list)); if (head.compareTo(threshold) > 0) { return true; } else { return anyGreaterThan(Builtins.tailList(list), threshold); } } } @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { PlutusData.ListData nums = Builtins.mkNilData(); nums = Builtins.mkCons(Builtins.iData(10), nums); nums = Builtins.mkCons(Builtins.iData(50), nums); nums = Builtins.mkCons(Builtins.iData(3), nums); long count = countElements(nums); boolean hasLarge = anyGreaterThan(nums, BigInteger.valueOf(25)); return count == 3 && hasLarge; } } ``` ##### GCD: Two-Parameter Recursion Recursive functions with multiple parameters work naturally: ```java @SpendingValidator class GcdValidator { static BigInteger gcd(BigInteger a, BigInteger b) { if (b.equals(BigInteger.ZERO)) { return a; } else { return gcd(b, a.remainder(b)); } } @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { BigInteger a = BigInteger.valueOf(48); BigInteger b = BigInteger.valueOf(18); BigInteger result = gcd(a, b); return result.equals(BigInteger.valueOf(6)); } } ``` ##### Multi-Binding LetRec (Mutual Recursion) JuLC supports mutual recursion between **two** methods via Bekic's theorem. This enables patterns like `isEven`/`isOdd`: ```java @SpendingValidator class MutualRecursion { // These two methods are mutually recursive static boolean isEven(BigInteger n) { if (n.equals(BigInteger.ZERO)) { return true; } else { return isOdd(n.subtract(BigInteger.ONE)); } } static boolean isOdd(BigInteger n) { if (n.equals(BigInteger.ZERO)) { return false; } else { return isEven(n.subtract(BigInteger.ONE)); } } @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { BigInteger n = Builtins.unIData(redeemer); return isEven(n); } } ``` ##### Limitations - **Self-recursion**: Fully supported for any number of helper methods. - **2-binding mutual recursion**: Supported via Bekic's theorem (e.g., `isEven`/`isOdd` above). - **>2-binding mutual recursion**: **Not supported.** If three or more methods form a mutual recursion cycle, the compiler will fail. Restructure your code to use at most two mutually recursive methods, or refactor into self-recursive form with a mode parameter: ```java // WORKAROUND for 3-way mutual recursion: // Combine into a single self-recursive function with a mode tag static BigInteger dispatch(BigInteger mode, BigInteger n) { if (mode.equals(BigInteger.ZERO)) { // was functionA return dispatch(BigInteger.ONE, n.subtract(BigInteger.ONE)); } else if (mode.equals(BigInteger.ONE)) { // was functionB return dispatch(BigInteger.TWO, n.subtract(BigInteger.ONE)); } else { // was functionC -- base case if (n.equals(BigInteger.ZERO)) { return BigInteger.ONE; } else { return dispatch(BigInteger.ZERO, n.subtract(BigInteger.ONE)); } } } ``` ##### Tail-Recursive Style with Accumulators For better budget efficiency, prefer tail-recursive accumulator style over naive recursion. While JuLC does not perform tail-call optimization, the accumulator pattern avoids building up a deep chain of deferred multiplications or additions: ```java @SpendingValidator class TailRecursive { // Tail-recursive factorial with accumulator static BigInteger factAcc(BigInteger n, BigInteger acc) { if (n.equals(BigInteger.ZERO)) { return acc; } else { return factAcc(n.subtract(BigInteger.ONE), acc.multiply(n)); } } // Tail-recursive list sum with accumulator static BigInteger sumAcc(PlutusData list, BigInteger acc) { if (Builtins.nullList(list)) { return acc; } else { BigInteger head = Builtins.unIData(Builtins.headList(list)); return sumAcc(Builtins.tailList(list), acc.add(head)); } } @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { BigInteger fact5 = factAcc(BigInteger.valueOf(5), BigInteger.ONE); PlutusData.ListData nums = Builtins.mkNilData(); nums = Builtins.mkCons(Builtins.iData(10), nums); nums = Builtins.mkCons(Builtins.iData(20), nums); BigInteger sum = sumAcc(nums, BigInteger.ZERO); return fact5.equals(BigInteger.valueOf(120)) && sum.equals(BigInteger.valueOf(30)); } } ``` Note: For iterative patterns (where you do not need true recursion), prefer `while` loops. The compiler desugars `while` loops into efficient `LetRec` with accumulator unpacking, which is often more budget-friendly than manual recursion for simple traversals. --- #### 13. @NewType Zero-Cost Type Aliases The `@NewType` annotation creates zero-cost type aliases for single-field records. On-chain, the constructor compiles to identity — no `ConstrData` wrapping is generated. ```java @NewType public record AssetClass(byte[] policyId) {} // Usage in validator: AssetClass ac = AssetClass.of(myBytes); // .of() auto-registered ``` **Constraints:** - Must be a `record` with exactly one field - Underlying type must be `byte[]`, `BigInteger`, `String`, or `boolean` - Multi-field or unsupported-type records produce a compiler error **On-chain behavior:** `AssetClass.of(bytes)` compiles to identity — the bytes are passed through as-is. Field access `ac.policyId()` is also identity. --- #### 14. Optional Support `Optional` is supported as a first-class on-chain type. It maps to `ConstrData`: - `Optional.of(x)` → `ConstrData(0, [encode(x)])` - `Optional.empty()` → `ConstrData(1, [])` ```java Optional maybe = Optional.of(BigInteger.valueOf(42)); if (maybe.isPresent()) { BigInteger val = maybe.get(); // auto-decoded from Data } ``` **Instance methods:** | Method | Description | |--------|-------------| | `.isPresent()` | True if Some (tag == 0) | | `.isEmpty()` | True if None (tag == 1) | | `.get()` | Unwrap the inner value (decoded based on type arg) | Use `import java.util.Optional` or bare `Optional` in your validator code. --- #### 15. Tuple2/Tuple3 Generic Support `Tuple2` and `Tuple3` provide generic tuples with auto-unwrapping field access based on type arguments. ```java Tuple2 result = MathLib.divMod(a, b); BigInteger quotient = result.first(); // auto-generates UnIData byte[] remainder = result.second(); // auto-generates UnBData // Construction auto-wraps var t = new Tuple2(val1, val2); // auto-wraps via IData ``` **Important:** Tuple2/Tuple3 are **not switchable** — they are registered as `RecordType`, but `switch` requires `SumType` (sealed interface). Use `.first()`, `.second()`, `.third()` field access instead of pattern matching. Raw `Tuple2` (no type args) defaults to `DataType` for backward compatibility. --- #### 16. Nested Loops Nested loops are fully supported: while-in-while, for-each-in-for-each, and mixed nesting. The `LoopDesugarer` assigns unique names to each loop function (`loop__forEach__0`, `loop__while__1`, etc.) to prevent name collisions. ```java BigInteger total = 0; for (var group : groups) { for (var item : group.items()) { total = total + item.amount(); } } ``` For detailed patterns including multi-accumulator nested loops, see [For-Loop Patterns](/guides/for-loop-patterns/). --- #### 17. Higher-Order Functions (HOFs) Higher-order functions are available as both instance methods and static calls. Lambda parameter types are auto-inferred from the list element type. ```java // Instance method syntax (preferred) boolean hasLargeAmount = outputs.any(o -> o.value().lovelaceOf() > 1000000); var filtered = list.filter(x -> x > 0); var mapped = list.map(x -> x + 1); // returns JulcList // Static syntax boolean found = ListsLib.any(list, x -> x > threshold); var sum = ListsLib.foldl((acc, x) -> acc + Builtins.unIData(x), BigInteger.ZERO, list); ``` **Available HOFs:** `map`, `filter`, `any`, `all`, `find` (instance + static), `foldl`, `zip` (static only). Variable capture is supported. Block body lambdas and chaining (`list.filter(...).map(...)`) work as expected. Note: `map()` wraps lambda results to Data — the returned list has `DataType` elements. Use `Builtins.unIData()` etc. when extracting values from mapped results. For the full HOF reference, see [Standard Library Guide — HOFs](/stdlib/stdlib-guide/). --- #### 18. Byte Array Constants JuLC supports `byte[]` constants in on-chain code using two syntaxes: ##### String.getBytes() ```java static final byte[] FACTORY_MARKER = "FACTORY_MARKER".getBytes(); static final byte[] LOTTERY_TOKEN = "LOTTERY_TOKEN".getBytes(); ``` Compiles to `EncodeUtf8("FACTORY_MARKER")` — produces the UTF-8 encoded bytes at UPLC level. ##### Literal byte[] Initializers ```java static final byte[] TOKEN_PREFIX = new byte[]{0x46, 0x41, 0x43, 0x54}; ``` Compiles to a `ByteString` constant. All array elements must be integer literals (no variables or expressions). ##### Usage in Validators ```java @MintingValidator class FactoryPolicy { static final byte[] FACTORY_MARKER = "FACTORY_MARKER".getBytes(); @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); byte[] ownPolicy = (byte[])(Object) ContextsLib.ownHash(ctx); BigInteger qty = ValuesLib.assetOf(txInfo.mint(), ownPolicy, FACTORY_MARKER); return qty.equals(BigInteger.ONE); } } ``` --- ## For-Each Loop Patterns Source: https://julc.dev/guides/for-loop-patterns/ > For-Each Loop Patterns - JuLC documentation JuLC compiles `for (var item : list)` loops into recursive folds over Data lists. Since UPLC has no mutable state, the compiler detects variables assigned inside the loop body ("accumulators") and threads them through the fold as functional accumulator parameters. This document covers every supported pattern, the compilation strategy behind each, and known limitations. #### How It Works The compiler analyzes each for-each loop body to determine the compilation path: | Accumulators Detected | Has `break` | Path | |-----------------------|-------------|------| | 0 | no | Unit-accumulator fold (side-effect only) | | 1 | no | Single-accumulator fold | | 1 | yes | Single-accumulator fold with break | | 2+ | no | Multi-accumulator tuple fold | | 2+ | yes | Multi-accumulator tuple fold with break | An **accumulator** is any variable declared **before** the loop and **assigned inside** it. #### Pattern 1: Side-Effect Loop (No Accumulator) When the loop body doesn't assign to any pre-loop variable, the compiler uses a unit accumulator. The body is evaluated for side-effects (e.g., trace logging) and the fold returns unit. ```java for (var sig : txInfo.signatories()) { ContextsLib.trace(sig); } ``` **Compiles to:** ``` LetRec([loop = \xs \acc -> if NullList(xs) then acc else loop(TailList(xs), body)], loop(signatories, Unit)) ``` #### Pattern 2: Single Accumulator (No Break) The most common pattern. A single pre-loop variable is updated inside the loop. The compiler detects the assignment and threads the variable as a fold accumulator. ##### Boolean accumulator ```java boolean found = false; for (var sig : txInfo.signatories()) { if (sig == redeemer) { found = true; } } return found; ``` ##### Integer accumulator ```java BigInteger total = BigInteger.ZERO; for (var output : txInfo.outputs()) { total = total + ValuesLib.lovelaceOf(output.value()); } return total; ``` ##### Conditional update ```java boolean found = false; for (var sig : txInfo.signatories()) { found = found || sig == redeemer; } return found; ``` **Compiles to:** ``` LetRec([loop = \xs \acc -> if NullList(xs) then acc else loop(TailList(xs), Let(item, HeadList(xs), bodyExpr))], loop(signatories, initAcc)) ``` After the loop, the accumulator is rebound to the fold result for use in subsequent statements. #### Pattern 3: Single Accumulator with Break `break` inside a for-each loop terminates iteration early. The compiler generates a break-aware fold where the loop body decides whether to recurse (continue) or return the accumulator directly (break). ##### Assignment + break inside if ```java boolean found = false; for (var sig : txInfo.signatories()) { if (sig == redeemer) { found = true; break; } } return found; ``` ##### Assignment before if-with-break The assignment can be a standalone statement before the `if`: ```java boolean found = false; for (var sig : txInfo.signatories()) { found = sig == redeemer; if (found) { break; } } return found; ``` ##### Integer accumulator with break ```java BigInteger sum = BigInteger.ZERO; for (var item : items) { sum = sum + item; if (sum > BigInteger.valueOf(100)) { break; } } return sum; ``` **Compiles to:** ``` LetRec([loop = \xs \acc -> if NullList(xs) then acc else Let(item, HeadList(xs), ... if breakCond then accValue // break: return directly else loop(TailList(xs), newAcc))], // continue: recurse loop(list, initAcc)) ``` #### Pattern 4: Multiple Accumulators (No Break) When two or more pre-loop variables are assigned inside the loop, the compiler packs them into a Data list tuple `[encode(v1), encode(v2), ...]`, folds with this single tuple, and unpacks after the loop. ```java boolean found = false; BigInteger count = BigInteger.ZERO; for (var sig : txInfo.signatories()) { found = found || sig == redeemer; count = count + BigInteger.ONE; } return found; ``` **Compiles to:** ``` // Init: pack [BoolToData(false), IData(0)] accInit = MkCons(ConstrData(0, []), MkCons(IData(0), MkNilData)) // Fold body: unpack, compute, repack loop = \xs \__acc_tuple -> if NullList(xs) then __acc_tuple else Let(item, HeadList(xs), Let(found, UnConstrData(HeadList(__acc_tuple)), Let(count, UnIData(HeadList(TailList(__acc_tuple))), ... compute new found, new count ... MkCons(encode(found'), MkCons(encode(count'), MkNilData))))) // After loop: unpack final state Let(__acc_tuple, loop(sigs, accInit), Let(found, decode(HeadList(__acc_tuple)), Let(count, decode(HeadList(TailList(__acc_tuple))), ... rest of validator ...))) ``` ##### Encoding/Decoding Types Each accumulator is encoded to Data and decoded back based on its type: | Type | Encode | Decode | |------|--------|--------| | `BigInteger` | `IData(value)` | `UnIData(data)` | | `byte[]` | `BData(value)` | `UnBData(data)` | | `boolean` | `ConstrData(tag, [])` | `FstPair(UnConstrData(data)) == 1` | | `String` | `BData(EncodeUtf8(value))` | `DecodeUtf8(UnBData(data))` | | `List` | `ListData(value)` | `UnListData(data)` | | `Map` | `MapData(value)` | `UnMapData(data)` | | `PlutusData`, records | passthrough | passthrough | #### Pattern 5: Multiple Accumulators with Break Combines multi-accumulator tuple packing with break-aware fold generation. ```java boolean found = false; BigInteger index = BigInteger.ZERO; for (var sig : txInfo.signatories()) { found = sig == redeemer; index = index + BigInteger.ONE; if (found) { break; } } return found; ``` At `break`, the current accumulator values are packed and returned (no recursion). At body end, they are packed and passed to the continue function (recursion). #### Pattern 6: Nested For-Each Loops For-each loops can be nested. The compiler saves and restores the accumulator context when entering an inner loop, so each loop operates independently. ```java BigInteger total = BigInteger.ZERO; for (var output : txInfo.outputs()) { boolean match = false; for (var sig : txInfo.signatories()) { if (sig == redeemer) { match = true; break; } } // match is available here from the inner loop total = total + BigInteger.ONE; } return total == BigInteger.ZERO; ``` The inner loop compiles as a single-accumulator fold with break. The outer loop sees `match` as a local variable and `total` as its own accumulator. #### While Loops While loops use the same accumulator detection as for-each loops. The compiler analyzes the while body for assignments to pre-loop variables and threads them as functional accumulator parameters through the recursive call. | Accumulators Detected | Has `break` | Path | |-----------------------|-------------|------| | 0 | no | Side-effect only (unit recursion) | | 1 | no | Single-accumulator recursion | | 1 | yes | Single-accumulator with break | | 2+ | no | Multi-accumulator tuple recursion | | 2+ | yes | Multi-accumulator tuple with break | ##### While Pattern 1: Side-Effect Loop (No Accumulator) When the while body doesn't assign to any pre-loop variable, the compiler uses a unit-based recursion. The body is evaluated for side-effects and the loop returns unit. ```java while (condition) { ContextsLib.trace(someValue); } ``` **Compiles to:** ``` LetRec([loop = \_ -> if cond then Let(_, body, loop(Unit)) else Unit], loop(Unit)) ``` ##### While Pattern 2: Single Accumulator (No Break) The most common while loop pattern. A single pre-loop variable is updated inside the loop, and the condition typically references the same variable. ###### Countdown ```java BigInteger k = BigInteger.valueOf(10); while (k > BigInteger.ZERO) { k = k - BigInteger.ONE; } // k is now 0 ``` ###### Boolean accumulator ```java boolean done = false; while (!done) { done = true; } return done; ``` **Compiles to:** ``` LetRec([loop = \acc -> if cond(acc) then loop(body(acc)) else acc], loop(initAcc)) ``` Both `cond` and `body` reference `acc` as a free variable. When the desugarer wraps them in `\acc -> ...`, the variable references bind to the lambda parameter. Each recursive call passes the new accumulator value. After the loop, the accumulator is rebound to the loop result for use in subsequent statements: ```java BigInteger k = BigInteger.valueOf(3); while (k > BigInteger.ZERO) { k = k - BigInteger.ONE; } BigInteger result = k + BigInteger.valueOf(100); // result is 100 (k was rebound to 0 after the loop) ``` ##### While Pattern 3: Single Accumulator with Break `break` inside a while loop terminates iteration early. The compiler generates a break-aware loop where the body decides whether to recurse (continue) or return the accumulator directly (break). ```java BigInteger k = BigInteger.valueOf(10); while (k > BigInteger.ZERO) { if (k == BigInteger.valueOf(5)) { break; } k = k - BigInteger.ONE; } // k is now 5 ``` **Compiles to:** ``` LetRec([loop = \acc -> if cond(acc) then bodyTerm(loop, acc) else acc], loop(initAcc)) ``` Where `bodyTerm` can either: - Call `loop(newAcc)` to continue iterating - Return `acc` directly to break out of the loop ##### While Pattern 4: Multiple Accumulators When two or more pre-loop variables are assigned inside the while body, the compiler packs them into a Data list tuple (same infrastructure as for-each multi-accumulator). ```java BigInteger sum = BigInteger.ZERO; BigInteger k = BigInteger.valueOf(5); while (k > BigInteger.ZERO) { sum = sum + k; k = k - BigInteger.ONE; } // sum is 15, k is 0 ``` The condition is wrapped with unpack logic so it can access individual accumulator values from the tuple. After the loop, the final tuple is unpacked back into the individual variables. ##### While Pattern 5: Multiple Accumulators with Break Combines multi-accumulator tuple packing with break-aware recursion. ```java BigInteger sum = BigInteger.ZERO; BigInteger k = BigInteger.valueOf(10); while (k > BigInteger.ZERO) { sum = sum + k; if (sum > BigInteger.valueOf(20)) { break; } k = k - BigInteger.ONE; } // sum > 20, k stopped early ``` At `break`, the current accumulator values are packed and returned (no recursion). At body end, they are packed and passed to the continue function (recursion). #### For-Each on MapType When iterating over a `Map` variable, the compiler auto-detects the `MapType`, prepends an `UnMapData` to convert to a pair list, and types each element as `PairType`. Use `.key()` and `.value()` to access pair elements: ```java // Iterate over withdrawals map BigInteger totalWithdrawn = BigInteger.ZERO; for (var entry : txInfo.withdrawals()) { // entry is a PairType — use .key() and .value() byte[] credHash = entry.key(); // auto-decoded BigInteger amount = entry.value(); // auto-decoded totalWithdrawn = totalWithdrawn + amount; } ``` #### Nested Loop Examples ##### While-in-While ```java BigInteger total = BigInteger.ZERO; BigInteger i = BigInteger.ZERO; while (i < BigInteger.valueOf(3)) { BigInteger j = BigInteger.ZERO; while (j < BigInteger.valueOf(4)) { total = total + BigInteger.ONE; j = j + BigInteger.ONE; } i = i + BigInteger.ONE; } // total is 12 ``` ##### For-Each-in-For-Each ```java BigInteger matchCount = BigInteger.ZERO; for (var output : txInfo.outputs()) { for (var sig : txInfo.signatories()) { if (output.address().credential() == sig) { matchCount = matchCount + BigInteger.ONE; } } } ``` ##### Mixed Nesting (For-Each with While) ```java boolean found = false; for (var input : txInfo.inputs()) { var pairs = Builtins.unMapData(input.resolved().value()); PlutusData cursor = pairs; while (!Builtins.nullList(cursor)) { var pair = Builtins.headList(cursor); if (Builtins.equalsData(Builtins.fstPair(pair), targetPolicy)) { found = true; } cursor = Builtins.tailList(cursor); } } ``` Each loop gets a unique counter-based name (`loop__forEach__0`, `loop__while__1`, etc.) to prevent naming collisions. Inner loop accumulators are correctly rebound into the outer loop's scope. #### Supported Accumulator Types Any type supported by the compiler can be used as a loop accumulator: - `boolean` — for search/match patterns - `BigInteger` (and `int`, `long`) — for counters, sums, products - `byte[]` — for hash accumulation - `String` — for string building - `PlutusData` — for opaque data threading - Records and sealed interfaces — for complex state #### Limitations | Pattern | Status | Notes | |---------|--------|-------| | `for (var x : list)` | Supported | Enhanced for-each only | | `while (cond) { ... }` | Supported | With accumulator threading | | `break` in for-each | Supported | Single and multi-accumulator | | `break` in while | Supported | Single and multi-accumulator | | `continue` | Not supported | Use conditional logic instead | | `for (int i = 0; ...)` | Rejected | C-style for loops not allowed | | `do { } while (cond)` | Rejected | Use `while` instead | | `break` outside loops | Rejected | Compile-time error | | Nested loops | Supported | While-in-while, for-each-in-for-each, mixed | | For-each on MapType | Supported | Elements are PairType with `.key()`/`.value()` | | Accumulator reassignment outside if | Supported | `acc = expr;` at any statement position | | Variable declaration inside loop | Supported | Local vars are scoped to the iteration | ##### Workaround for `continue` Instead of `continue`, use an `if` to skip the rest of the body: ```java // Instead of: if (cond) { continue; } // Use: BigInteger sum = BigInteger.ZERO; for (var item : items) { if (!skipCondition) { sum = sum + item; } } ``` ##### Immutability Reminder Variables in JuLC are immutable. The `acc = expr` syntax inside loops is special — the compiler recognizes it as a fold accumulator update, not a true mutation. Outside of loop bodies, assignment (`x = x + 1`) is not supported. ##### Post-Loop Variable Access in Multi-Accumulator Loops (FIXED) This bug has been fixed. Variables defined before a multi-accumulator while or for-each loop are now correctly accessible after the loop completes. The fix snapshots pre-loop variables via `SymbolTable.allVisibleVariables()` and re-binds them after accumulator unpacking using `rebindPreLoopVars()`. Previously, the LetRec transformation restructured the variable binding environment, causing outer-scope bindings to be lost. This no longer occurs for either single-accumulator or multi-accumulator loops. ##### No `return` Inside Multi-Accumulator Loop Body The compiler does not support `return` statements inside the body of a multi-accumulator loop. The LetRec transformation wraps the loop body into a fold function, and an early `return` would exit the fold lambda rather than the enclosing method. **Not supported:** ```java BigInteger sum = BigInteger.ZERO; boolean found = false; for (var item : items) { sum = sum + item; if (sum > BigInteger.valueOf(100)) { found = true; return found; // ERROR: return inside multi-acc loop body } } ``` **Workaround:** Use `break` to exit the loop early, then `return` after the loop: ```java BigInteger sum = BigInteger.ZERO; boolean found = false; for (var item : items) { sum = sum + item; if (sum > BigInteger.valueOf(100)) { found = true; break; } } return found; ``` ##### Cross-Method Type Inference for Primitives When a method calls a helper that accepts a `long` parameter, the compiler may generate `EqualsData` instead of `EqualsInteger` for comparisons inside the helper. This happens because at the UPLC level, cross-method values are passed as generic Data, and the compiler does not always recover the primitive type. **Problem pattern:** ```java boolean validate(PlutusData datum, PlutusData redeemer) { long amount = Builtins.unIData(Builtins.headList(Builtins.constrFields(datum))); return checkAmount(amount); } static boolean checkAmount(long amount) { // May generate EqualsData instead of EqualsInteger return amount > 0; } ``` **Workaround:** Keep primitive comparisons in the same method, or use Data-level equality when crossing method boundaries: ```java boolean validate(PlutusData datum, PlutusData redeemer) { long amount = Builtins.unIData(Builtins.headList(Builtins.constrFields(datum))); // Compare directly here — same method, correct type inference return amount > 0; } ``` ##### `@Param` Fields Must Use `PlutusData` Type `@Param` values are **always** raw Data at runtime, regardless of the declared type. Using `PlutusData.BytesData` (or `PlutusData.MapData`, etc.) on a `@Param` field tells the compiler the value is already a ByteString, which causes double-wrapping and incorrect cross-library calls. **Broken pattern:** ```java @Param PlutusData.BytesData myPolicyId; // WRONG — compiler thinks it's a ByteString boolean validate(PlutusData datum, PlutusData redeemer) { // Builtins.bData(myPolicyId) double-wraps: bData applied to Data, not ByteString byte[] pid = Builtins.unBData(myPolicyId); // Also fails — unBData on raw Data return true; } ``` **Correct pattern:** ```java @Param PlutusData myPolicyId; // CORRECT — raw Data, as it actually is at runtime boolean validate(PlutusData datum, PlutusData redeemer) { byte[] pid = Builtins.unBData(myPolicyId); // Works — unBData on Data return true; } ``` Always use `@Param PlutusData` for parameterized fields. ##### Cross-Library `BytesData`/`MapData` Parameter Bug When calling a stdlib library method that accepts `BytesData` or `MapData` typed parameters from user code, the compiler may skip the necessary Data encoding at the call boundary. This happens because the compiler sees matching types and assumes no conversion is needed, but compiled libraries always expect raw Data arguments at the UPLC boundary. **Problem pattern:** ```java PlutusData.BytesData myPolicy = ...; // typed as BytesData in user code // ValuesLib.assetOf expects BytesData, compiler sees matching types, skips encoding // But at UPLC level, the library expects raw Data -> type mismatch long amount = ValuesLib.assetOf(value, myPolicy, tokenName); ``` **Workaround:** Use `PlutusData` typed variables (not `BytesData`/`MapData`) when passing arguments to stdlib library methods, so the compiler passes Data as-is: ```java PlutusData myPolicy = ...; // typed as PlutusData — compiler passes raw Data long amount = ValuesLib.assetOf(value, myPolicy, tokenName); // Works correctly ``` Alternatively, create a local wrapper method in the same project that calls the stdlib method: ```java // Local wrapper — same compilation unit, no cross-library boundary static long localAssetOf(PlutusData value, PlutusData policyId, PlutusData tokenName) { return ValuesLib.assetOf(value, policyId, tokenName); } ``` #### Comparison with Other Cardano Languages | Feature | JuLC (Java) | Opshin (Python) | Aiken | Scalus (Scala) | |---------|-------------|-----------------|-------|----------------| | For-each | `for (var x : list)` | `for x in list:` | `list.fold(...)` | `list.foldLeft(...)` | | While with accumulator | Supported | Supported | N/A (no loops) | N/A (no loops) | | Multi-accumulator | Auto-detected tuple | Auto-detected tuple | Manual tuple | Manual tuple | | `break` in for-each | Supported | Not supported | N/A | N/A | | `break` in while | Supported | Not supported | N/A | N/A | | `continue` | Not supported | Not supported | N/A | N/A | --- ## Testing Guide Source: https://julc.dev/guides/testing-guide/ > Testing Guide - JuLC documentation Write tests for your Cardano smart contracts at every level — from direct Java debugging to property-based fuzzing to on-chain integration. Smart contracts handle real value and, once deployed, cannot be patched. JuLC provides a layered testing approach so you can catch bugs early: 1. **Direct Java tests** — call validator logic as plain Java, set breakpoints 2. **UPLC evaluation tests** — compile to Plutus and evaluate in the CEK machine 3. **Property-based tests** — run hundreds of random scenarios with jqwik 4. **Budget tests** — verify CPU/memory costs stay within bounds 5. **Integration tests** — submit real transactions to a local devnet | Layer | Speed | What it catches | |-------|-------|-----------------| | Direct Java | Instant | Logic bugs, wrong conditions (e.g., `>` vs `>=`), incorrect boundary checks | | UPLC evaluation | Fast (~ms) | Compilation issues, on-chain behavior differences | | Property-based | Moderate (~s) | Edge cases you didn't think of | | Budget | Fast (~ms) | Cost regressions, script size bloat | | Integration | Slow (~s) | Transaction building, serialization, ledger rules | #### 1. Project Setup ##### Gradle Add these test dependencies to your `build.gradle`: ```groovy dependencies { // Core compilation + ledger types implementation 'com.bloxbean.cardano:julc-core:' implementation 'com.bloxbean.cardano:julc-compiler:' implementation 'com.bloxbean.cardano:julc-ledger-api:' implementation 'com.bloxbean.cardano:julc-stdlib:' // Test framework testImplementation 'com.bloxbean.cardano:julc-testkit:' testRuntimeOnly 'com.bloxbean.cardano:julc-vm-scalus:' } ``` To add property-based testing with jqwik: ```groovy dependencies { testImplementation 'com.bloxbean.cardano:julc-testkit-jqwik:' testImplementation 'net.jqwik:jqwik:1.9.2' } ``` > If you use the JuLC BOM (`julc-bom`), you can omit version numbers for JuLC > modules. The BOM manages all versions centrally. ##### JUnit 5 Platform Both JUnit 5 and jqwik run on the JUnit Platform. Ensure your `build.gradle` includes: ```groovy test { useJUnitPlatform() } ``` ##### Maven ```xml com.bloxbean.cardano julc-testkit test com.bloxbean.cardano julc-vm-scalus test com.bloxbean.cardano julc-testkit-jqwik test net.jqwik jqwik 1.9.2 test ``` --- #### 2. Unit Testing with ContractTest `ContractTest` is the recommended base class for validator tests. It provides a pre-configured VM, compilation helpers, context builders, and assertion methods. In a real project, your validators live in their own `.java` files (e.g. `src/main/java/com/example/VestingValidator.java`). The test compiles the validator class directly — no need to copy the source into a string. Given a validator like this: ```java // src/main/java/com/example/VestingValidator.java package com.example; @SpendingValidator class VestingValidator { record VestingDatum(byte[] beneficiary, BigInteger deadline) {} @Entrypoint static boolean validate(VestingDatum datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); var sigs = txInfo.signatories(); return sigs.contains(datum.beneficiary()) && datum.deadline() > 0; } } ``` The test compiles it by class reference: ```java import com.bloxbean.cardano.julc.testkit.ContractTest; import com.bloxbean.cardano.julc.testkit.BudgetAssertions; import org.junit.jupiter.api.*; class VestingValidatorTest extends ContractTest { // Compile once — auto-discovers source file + @OnchainLibrary dependencies static CompileResult compiled; @BeforeAll static void setup() { initCrypto(); compiled = compileValidator(VestingValidator.class); } @Test void beneficiaryCanUnlock() { var ctx = buildSpendingCtx(beneficiaryPkh); assertValidates(compiled.program(), ctx); } @Test void attackerIsRejected() { var ctx = buildSpendingCtx(attackerPkh); assertFailure(evaluate(compiled.program(), ctx)); } } ``` `compileValidator(Class)` locates the `.java` source file on the classpath, discovers any `@OnchainLibrary` dependencies, and compiles everything together. This is the recommended approach for real projects. ##### ContractTest API Reference | Method | Description | |--------|-------------| | `compile(String source)` | Compile Java source to a `Program` | | `compile(String source, String... libs)` | Multi-file compilation | | `compile(Path sourceFile)` | Compile from a file path | | `compileValidator(Class)` | Auto-discover source + dependencies | | `compileValidatorWithSourceMap(Class)` | Compile with source map for error locations | | `evaluate(Program, PlutusData...)` | Evaluate a compiled program | | `evaluateWithTrace(CompileResult, PlutusData...)` | Evaluate with execution tracing | | `assertValidates(Program, PlutusData...)` | Assert the validator accepts | | `assertSuccess(EvalResult)` | Assert evaluation succeeded | | `assertFailure(EvalResult)` | Assert evaluation failed | | `assertSuccess(EvalResult, SourceMap)` | Assert success with source location on failure | | `assertBudgetUnder(EvalResult, long, long)` | Assert CPU/memory within bounds | | `spendingContext(TxOutRef)` | Create spending ScriptContext builder | | `spendingContext(TxOutRef, PlutusData)` | Spending builder with inline datum | | `mintingContext(PolicyId)` | Create minting ScriptContext builder | | `rewardingContext(Credential)` | Create rewarding ScriptContext builder | | `certifyingContext(BigInteger, TxCert)` | Create certifying context builder | | `votingContext(Voter)` | Create voting context builder | | `proposingContext(BigInteger, ProposalProcedure)` | Create proposing context builder | | `initCrypto()` | Initialize JVM crypto provider (call in `@BeforeAll`) | | `vm()` | Access the underlying `JulcVm` instance | ##### The Two-Tier Test Pattern A common pattern is to organize tests into two `@Nested` classes: 1. **DirectJavaTests** — call validator logic as plain Java for debugging 2. **UplcTests** — compile to UPLC and evaluate in the CEK machine ```java class VestingTest extends ContractTest { @BeforeAll static void setup() { initCrypto(); } // The EXACT validator logic, written as a regular method for debugging static boolean vestingValidate(byte[] beneficiary, BigInteger deadline, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); boolean isSigned = ContextsLib.signedBy(txInfo, beneficiary); boolean isPast = IntervalLib.contains(txInfo.validRange(), deadline); return isSigned && isPast; } @Nested class DirectJavaTests { @Test void beneficiaryCanUnlock() { var ctx = buildCtx(new byte[][]{pkh}, Interval.after(BigInteger.valueOf(500))); // SET BREAKPOINT HERE — step into vestingValidate with IntelliJ debugger! boolean result = vestingValidate(pkh, BigInteger.valueOf(1000), ctx); assertTrue(result); } @Test void wrongSignerRejected() { var ctx = buildCtx(new byte[][]{attacker}, Interval.after(BigInteger.valueOf(500))); assertFalse(vestingValidate(beneficiary, BigInteger.valueOf(1000), ctx)); } } @Nested class UplcTests { // Compile from the actual validator class static final CompileResult compiled = ValidatorTest.compileValidator(VestingValidator.class); @Test void beneficiaryCanUnlock() { var ctx = ScriptContextTestBuilder.spending(spentRef, datum) .signer(beneficiaryPkh) .input(txIn) .buildPlutusData(); BudgetAssertions.assertSuccess(ValidatorTest.evaluate(compiled.program(), ctx)); } } } ``` Direct Java tests let you set breakpoints and step through logic in your IDE. UPLC tests verify the same logic compiles correctly and behaves identically on-chain. --- #### 3. Building Test ScriptContexts The `ScriptContextTestBuilder` provides a fluent API for constructing Plutus V3 ScriptContexts with the correct structure. ##### Spending Context ```java import com.bloxbean.cardano.julc.testkit.ScriptContextTestBuilder; import com.bloxbean.cardano.julc.testkit.TestDataBuilder; import com.bloxbean.cardano.julc.ledger.*; var spentRef = TestDataBuilder.randomTxOutRef_typed(); var beneficiary = TestDataBuilder.randomPubKeyHash_typed(); var datum = PlutusData.constr(0, PlutusData.bytes(beneficiary.hash()), PlutusData.integer(1000)); var ctx = ScriptContextTestBuilder.spending(spentRef, datum) .signer(beneficiary) .input(TestDataBuilder.txIn(spentRef, TestDataBuilder.txOut( TestDataBuilder.pubKeyAddress(beneficiary), Value.lovelace(BigInteger.valueOf(5_000_000))))) .output(new TxOut(destAddress, destValue, new OutputDatum.NoOutputDatum(), Optional.empty())) .fee(BigInteger.valueOf(200_000)) .validRange(Interval.after(BigInteger.valueOf(1000))) .buildPlutusData(); ``` ##### Minting Context ```java var policyId = PolicyId.of(policyBytes); var ctx = ScriptContextTestBuilder.minting(policyId) .redeemer(PlutusData.bytes(signerPkh.hash())) .signer(signerPkh) .mint(Value.singleton(policyId, tokenName, BigInteger.ONE)) .input(TestDataBuilder.txIn( TestDataBuilder.randomTxOutRef_typed(), TestDataBuilder.txOut( TestDataBuilder.pubKeyAddress(signerPkh), Value.lovelace(BigInteger.valueOf(10_000_000))))) .buildPlutusData(); ``` ##### Other Script Purposes ```java // Rewarding (staking) ScriptContextTestBuilder.rewarding(credential) // Certifying (certificate operations) ScriptContextTestBuilder.certifying(BigInteger.ZERO, txCert) // Voting (Conway governance) ScriptContextTestBuilder.voting(voter) // Proposing (governance proposals) ScriptContextTestBuilder.proposing(BigInteger.ZERO, proposalProcedure) ``` ##### Builder Methods | Method | Description | |--------|-------------| | `.signer(PubKeyHash)` | Add a signatory | | `.signer(byte[])` | Add a signatory from raw bytes | | `.input(TxInInfo)` | Add a transaction input | | `.referenceInput(TxInInfo)` | Add a reference input | | `.output(TxOut)` | Add a transaction output | | `.fee(BigInteger)` | Set the fee (lovelace) | | `.mint(Value)` | Set the mint value | | `.validRange(Interval)` | Set the validity interval | | `.redeemer(PlutusData)` | Set the redeemer | | `.txId(TxId)` | Set the transaction ID | | `.certificate(TxCert)` | Add a certificate | | `.withdrawal(Credential, BigInteger)` | Add a withdrawal | | `.datum(DatumHash, PlutusData)` | Add a datum entry | | `.redeemerEntry(ScriptPurpose, PlutusData)` | Add a redeemer map entry | | `.currentTreasuryAmount(BigInteger)` | Set current treasury amount (Conway) | | `.treasuryDonation(BigInteger)` | Set treasury donation (Conway) | ##### Build Modes | Method | Returns | Use when | |--------|---------|----------| | `.build()` | `ScriptContext` | Direct Java tests with ledger types | | `.buildOnchain()` | `ScriptContext` | On-chain API variant | | `.buildPlutusData()` | `PlutusData` | UPLC evaluation via `ValidatorTest.evaluate()` | ##### TestDataBuilder Helpers `TestDataBuilder` provides random and typed test data generators: ```java import com.bloxbean.cardano.julc.testkit.TestDataBuilder; // Random typed ledger values PubKeyHash pkh = TestDataBuilder.randomPubKeyHash_typed(); TxOutRef ref = TestDataBuilder.randomTxOutRef_typed(); Address addr = TestDataBuilder.pubKeyAddress(pkh); TxOut out = TestDataBuilder.txOut(addr, Value.lovelace(BigInteger.valueOf(5_000_000))); TxInInfo input = TestDataBuilder.txIn(ref, out); // Raw PlutusData values PlutusData pkhData = TestDataBuilder.randomPubKeyHash(); PlutusData txIdData = TestDataBuilder.randomTxId(); PlutusData refData = TestDataBuilder.randomTxOutRef(); PlutusData unit = TestDataBuilder.unitData(); PlutusData boolTrue = TestDataBuilder.boolData(true); PlutusData someBytes = TestDataBuilder.randomBytes(32); // PlutusData constructors PlutusData anInt = TestDataBuilder.intData(42); PlutusData bytes = TestDataBuilder.bytesData(new byte[]{1, 2, 3}); PlutusData list = TestDataBuilder.listData(TestDataBuilder.intData(1), TestDataBuilder.intData(2)); PlutusData constr = TestDataBuilder.constrData(0, TestDataBuilder.intData(100)); PlutusData map = TestDataBuilder.mapData(TestDataBuilder.intData(1), TestDataBuilder.intData(2)); ``` --- #### 4. Testing Helper Methods with JulcEval `JulcEval` tests individual methods in isolation — no ScriptContext needed. It compiles a single Java method to UPLC, evaluates it, and extracts the result with natural Java types. ##### Interface Proxy Mode Define a Java interface matching the on-chain methods and call them directly: ```java import com.bloxbean.cardano.julc.testkit.JulcEval; // Given an on-chain class: // class MathHelper { // static BigInteger doubleIt(BigInteger x) { return x * 2; } // static boolean isPositive(BigInteger x) { return x > 0; } // } interface MathProxy { BigInteger doubleIt(long x); boolean isPositive(long x); } var proxy = JulcEval.forClass(MathHelper.class).create(MathProxy.class); assertEquals(BigInteger.valueOf(42), proxy.doubleIt(21)); assertTrue(proxy.isPositive(1)); assertFalse(proxy.isPositive(-5)); ``` The proxy automatically converts Java arguments (`long`, `int`, `byte[]`, etc.) to `PlutusData`, compiles and evaluates the method in the CEK machine, and converts the UPLC result back to the declared Java return type. ##### Fluent call() API For one-off calls without defining an interface: ```java var eval = JulcEval.forClass(MathHelper.class); BigInteger result = eval.call("doubleIt", 21).asInteger(); boolean positive = eval.call("isPositive", 1).asBoolean(); byte[] hash = eval.call("hashData", someBytes).asByteString(); ``` ##### Inline Source You can also test inline Java source strings: ```java var eval = JulcEval.forSource(""" class Utils { static BigInteger add(BigInteger a, BigInteger b) { return a + b; } } """); assertEquals(BigInteger.valueOf(30), eval.call("add", 10, 20).asInteger()); ``` ##### CallResult Extraction | Method | Return type | Description | |--------|-------------|-------------| | `.asInteger()` | `BigInteger` | Integer result | | `.asLong()` | `long` | Integer as long | | `.asInt()` | `int` | Integer as int | | `.asByteString()` | `byte[]` | Byte string result | | `.asBoolean()` | `boolean` | Boolean result | | `.asString()` | `String` | UTF-8 string result | | `.asData()` | `PlutusData` | Raw PlutusData | | `.asOptional()` | `Optional` | Optional (Some/None) | | `.asList()` | `List` | List of data items | | `.as(Class)` | `T` | Any ledger type or primitive | | `.auto()` | `Object` | Auto-detect type | | `.rawTerm()` | `Term` | Raw UPLC term | ##### Source Maps with JulcEval Enable source maps to get Java line numbers in error messages: ```java var eval = JulcEval.forClass(SwapOrder.class).sourceMap(); // On failure: "Evaluation failed at SwapOrder.java:42 (Builtins.error())" ``` ##### Execution Tracing Enable tracing for step-by-step execution analysis: ```java var eval = JulcEval.forClass(MyValidator.class).trace(); eval.call("validate", args); // Print execution trace System.out.println(eval.formatLastTrace()); System.out.println(eval.formatLastBudgetSummary()); ``` --- #### 5. ValidatorTest — Static Utility API `ValidatorTest` provides static methods for compile-and-evaluate workflows without extending a base class. Use it when you prefer composition over inheritance. ##### Compile ```java import com.bloxbean.cardano.julc.testkit.ValidatorTest; // From class — recommended for real projects // Auto-discovers source file + @OnchainLibrary dependencies CompileResult result = ValidatorTest.compileValidator(MyValidator.class); Program program = result.program(); // From fully-qualified class name (when .class is unavailable, e.g. -proc:only builds) CompileResult result = ValidatorTest.compileValidatorByName("com.example.MyValidator"); // From a .java file path Program program = ValidatorTest.compile(Path.of("src/main/java/MyValidator.java")); // From inline source string (useful for quick experiments and internal compiler tests) Program program = ValidatorTest.compile(javaSource); // Multi-file inline sources (validator + library) Program program = ValidatorTest.compile(validatorSource, librarySource); ``` ##### Evaluate ```java // Evaluate with PlutusData arguments EvalResult result = ValidatorTest.evaluate(program, datum, redeemer, scriptContext); // Evaluate with an explicit budget cap EvalResult result = ValidatorTest.evaluate(program, new ExBudget(10_000_000_000L, 10_000_000L), datum, redeemer, scriptContext); // Assert success/failure ValidatorTest.assertValidates(program, datum, redeemer, ctx); ValidatorTest.assertRejects(program, datum, redeemer, ctx); ``` ##### Evaluate Individual Methods ```java // Evaluate a single static method BigInteger result = ValidatorTest.evaluateInteger(javaSource, "myMethod", arg1, arg2); boolean check = ValidatorTest.evaluateBoolean(javaSource, "isValid", arg1); PlutusData data = ValidatorTest.evaluateData(javaSource, "transform", input); // File-based method evaluation BigInteger result = ValidatorTest.evaluateInteger(MathHelper.class, "doubleIt", arg); ``` --- #### 6. Budget and Trace Assertions Budget testing catches cost regressions before they become on-chain failures. Script size testing ensures your validators fit within the 16 KB Plutus limit. ##### BudgetAssertions API ```java import com.bloxbean.cardano.julc.testkit.BudgetAssertions; var result = ValidatorTest.evaluate(program, ctx); // Success / failure BudgetAssertions.assertSuccess(result); BudgetAssertions.assertFailure(result); // CPU and memory limits BudgetAssertions.assertBudgetUnder(result, 50_000_000L, 200_000L); // Trace messages (from Builtins.trace()) BudgetAssertions.assertTrace(result, "checking signature"); BudgetAssertions.assertTraceExact(result, "step1", "step2", "step3"); BudgetAssertions.assertNoTraces(result); // Script size (compile-time check) var compiled = ValidatorTest.compileWithDetails(source); BudgetAssertions.assertScriptSizeUnder(compiled, 16_384); // 16 KB max ``` ##### Source Map Assertions When a test fails, source-map-aware assertions include the Java source location: ```java var compiled = ValidatorTest.compileValidatorWithSourceMap(MyValidator.class); var result = ValidatorTest.evaluate(compiled.program(), ctx); // Throws with "Expected success, but failed at MyValidator.java:42" BudgetAssertions.assertSuccess(result, compiled.sourceMap()); ``` ##### Budget Regression Test Pattern Record budget values as constants and assert they don't regress: ```java // Compile once from class static final CompileResult compiled = ValidatorTest.compileValidator(MyValidator.class); // Known-good budget for this validator (from initial measurement) static final long MAX_CPU = 25_000_000L; static final long MAX_MEM = 100_000L; @Test void budgetDoesNotRegress() { var ctx = buildTypicalContext(); var result = ValidatorTest.evaluate(compiled.program(), ctx); BudgetAssertions.assertSuccess(result); BudgetAssertions.assertBudgetUnder(result, MAX_CPU, MAX_MEM); } ``` ##### Script Size Analysis ```java import com.bloxbean.cardano.julc.testkit.ScriptAnalysis; var compiled = ValidatorTest.compileValidator(MyValidator.class); var analysis = ScriptAnalysis.of(compiled); System.out.println("FLAT size: " + analysis.flatSizeFormatted()); // e.g. "3.2 KB" if (analysis.exceedsMaxSize()) { fail("Script exceeds 16 KB limit: " + analysis.flatSizeFormatted()); } ``` ##### Budget Logging Helper A simple pattern for printing budgets during development: ```java static void logBudget(String name, EvalResult result) { var budget = result.budgetConsumed(); System.out.printf("%s: CPU=%,d Mem=%,d%n", name, budget.cpuSteps(), budget.memoryUnits()); } ``` --- #### 7. Property-Based Testing with jqwik Property-based testing generates hundreds of random inputs to verify that invariants always hold. For smart contracts, this is invaluable — it finds edge cases that hand-written tests miss. ##### What is a Property? Instead of "given this specific input, expect this specific output", a property says "for ALL valid inputs, this invariant holds": - "The beneficiary can always unlock their vesting contract" - "An unauthorized signer can never mint tokens" - "The evaluation budget never exceeds X CPU steps" ##### Setup Add `julc-testkit-jqwik` and `jqwik` to your test dependencies: ```groovy testImplementation 'com.bloxbean.cardano:julc-testkit-jqwik:' testImplementation 'net.jqwik:jqwik:1.9.2' ``` ##### Complete Example: Vesting Validator Properties Assume `VestingValidator.java` is your validator class in `src/main/java`: ```java // src/main/java/com/example/VestingValidator.java @SpendingValidator class VestingValidator { record VestingDatum(PlutusData beneficiary, PlutusData deadline) {} @Entrypoint static boolean validate(VestingDatum datum, PlutusData redeemer, PlutusData ctx) { PlutusData txInfo = ContextsLib.getTxInfo(ctx); PlutusData pkh = datum.beneficiary(); return ContextsLib.signedBy(txInfo, pkh); } } ``` The property test compiles it once and runs hundreds of random scenarios: ```java import com.bloxbean.cardano.julc.compiler.CompileResult; import com.bloxbean.cardano.julc.core.PlutusData; import com.bloxbean.cardano.julc.core.Program; import com.bloxbean.cardano.julc.ledger.*; import com.bloxbean.cardano.julc.testkit.BudgetAssertions; import com.bloxbean.cardano.julc.testkit.ScriptContextTestBuilder; import com.bloxbean.cardano.julc.testkit.TestDataBuilder; import com.bloxbean.cardano.julc.testkit.ValidatorTest; import com.bloxbean.cardano.julc.testkit.jqwik.BudgetCollector; import com.bloxbean.cardano.julc.testkit.jqwik.CardanoArbitraries; import net.jqwik.api.*; import net.jqwik.api.lifecycle.AfterProperty; import java.math.BigInteger; import java.util.Arrays; class VestingPropertyTest { // Compile once from class — reuse across all property trials static final Program VESTING = ValidatorTest.compileValidator(VestingValidator.class).program(); final BudgetCollector budgetCollector = new BudgetCollector(); /** * Property: the beneficiary can ALWAYS unlock the vesting contract. */ @Property(tries = 200) void beneficiaryAlwaysSucceeds(@ForAll("pkh") PubKeyHash beneficiary) { var ctx = buildVestingCtx(beneficiary, beneficiary); var result = ValidatorTest.evaluate(VESTING, ctx); BudgetAssertions.assertSuccess(result); budgetCollector.record(result); } /** * Property: a non-beneficiary can NEVER unlock the vesting contract. */ @Property(tries = 200) void nonBeneficiaryAlwaysFails( @ForAll("pkh") PubKeyHash beneficiary, @ForAll("pkh") PubKeyHash attacker) { Assume.that(!Arrays.equals(beneficiary.hash(), attacker.hash())); var ctx = buildVestingCtx(beneficiary, attacker); var result = ValidatorTest.evaluate(VESTING, ctx); BudgetAssertions.assertFailure(result); } /** * Property: evaluation budget is always bounded. */ @Property(tries = 200) void budgetIsBounded(@ForAll("pkh") PubKeyHash beneficiary) { var ctx = buildVestingCtx(beneficiary, beneficiary); var result = ValidatorTest.evaluate(VESTING, ctx); BudgetAssertions.assertSuccess(result); BudgetAssertions.assertBudgetUnder(result, 50_000_000, 200_000); } @AfterProperty void reportBudget() { if (budgetCollector.count() > 0) { System.out.println(budgetCollector.summary()); } } @Provide Arbitrary pkh() { return CardanoArbitraries.pubKeyHash(); } private static PlutusData buildVestingCtx(PubKeyHash beneficiary, PubKeyHash signer) { var datum = PlutusData.constr(0, PlutusData.bytes(beneficiary.hash()), PlutusData.integer(1000)); var spentRef = TestDataBuilder.randomTxOutRef_typed(); return ScriptContextTestBuilder.spending(spentRef, datum) .signer(signer) .input(TestDataBuilder.txIn(spentRef, TestDataBuilder.txOut( TestDataBuilder.pubKeyAddress(beneficiary), Value.lovelace(BigInteger.valueOf(5_000_000))))) .buildPlutusData(); } } ``` Key points: - **Compile once from class**: `compileValidator(VestingValidator.class).program()` avoids recompiling every trial - **`@Property(tries = 200)`**: each property runs 200 random trials - **`@ForAll("pkh")`**: injects random `PubKeyHash` via `@Provide` method - **`Assume.that(...)`**: skip trials where beneficiary == attacker (distinct keys) - **`BudgetCollector`**: accumulates budget stats across trials ##### Complete Example: Minting Policy Properties Assume `SignedMintPolicy.java` is your minting policy in `src/main/java`: ```java // src/main/java/com/example/SignedMintPolicy.java @MintingPolicy class SignedMintPolicy { @Entrypoint static boolean validate(PlutusData redeemer, PlutusData ctx) { PlutusData txInfo = ContextsLib.getTxInfo(ctx); return ContextsLib.signedBy(txInfo, redeemer); } } ``` The property test: ```java class MintingPropertyTest { // Compile once from class static final Program MINTING_POLICY = ValidatorTest.compileValidator(SignedMintPolicy.class).program(); final BudgetCollector budgetCollector = new BudgetCollector(); @Property(tries = 200) void authorizedSignerCanMint( @ForAll("policyId") PolicyId policy, @ForAll("pkh") PubKeyHash signer, @ForAll("tokenName") TokenName tokenName) { var ctx = ScriptContextTestBuilder.minting(policy) .redeemer(PlutusData.bytes(signer.hash())) .signer(signer) .mint(Value.singleton(policy, tokenName, BigInteger.ONE)) .input(TestDataBuilder.txIn( TestDataBuilder.randomTxOutRef_typed(), TestDataBuilder.txOut( TestDataBuilder.pubKeyAddress(signer), Value.lovelace(BigInteger.valueOf(10_000_000))))) .buildPlutusData(); var result = ValidatorTest.evaluate(MINTING_POLICY, ctx); BudgetAssertions.assertSuccess(result); budgetCollector.record(result); } @Property(tries = 200) void unauthorizedSignerCannotMint( @ForAll("policyId") PolicyId policy, @ForAll("pkh") PubKeyHash requiredSigner, @ForAll("pkh") PubKeyHash actualSigner) { Assume.that(!Arrays.equals(requiredSigner.hash(), actualSigner.hash())); var ctx = ScriptContextTestBuilder.minting(policy) .redeemer(PlutusData.bytes(requiredSigner.hash())) .signer(actualSigner) .mint(Value.singleton(policy, TokenName.EMPTY, BigInteger.ONE)) .input(TestDataBuilder.txIn( TestDataBuilder.randomTxOutRef_typed(), TestDataBuilder.txOut( TestDataBuilder.pubKeyAddress(actualSigner), Value.lovelace(BigInteger.valueOf(10_000_000))))) .buildPlutusData(); var result = ValidatorTest.evaluate(MINTING_POLICY, ctx); BudgetAssertions.assertFailure(result); } @AfterProperty void reportBudget() { if (budgetCollector.count() > 0) { System.out.println(budgetCollector.summary()); } } @Provide Arbitrary pkh() { return CardanoArbitraries.pubKeyHash(); } @Provide Arbitrary policyId() { return CardanoArbitraries.policyId(); } @Provide Arbitrary tokenName() { return CardanoArbitraries.tokenName(); } } ``` ##### CardanoArbitraries — Available Generators The `CardanoArbitraries` class provides jqwik `Arbitrary` generators for all Cardano/Plutus types. **Hash types:** | Generator | Type | Size | |-----------|------|------| | `pubKeyHash()` | `PubKeyHash` | 28 bytes | | `scriptHash()` | `ScriptHash` | 28 bytes | | `validatorHash()` | `ValidatorHash` | 28 bytes | | `policyId()` | `PolicyId` | 28 bytes | | `tokenName()` | `TokenName` | 0-32 bytes | | `datumHash()` | `DatumHash` | 32 bytes | | `txId()` | `TxId` | 32 bytes | **Composite types:** | Generator | Type | Notes | |-----------|------|-------| | `credential()` | `Credential` | PubKeyCredential or ScriptCredential | | `address()` | `Address` | Enterprise address with credential | | `txOutRef()` | `TxOutRef` | TxId + index (0-9) | | `lovelaceValue()` | `Value` | 1-100 ADA | | `lovelaceValue(min, max)` | `Value` | Bounded lovelace | | `multiAssetValue()` | `Value` | ADA + 1-3 native tokens | | `value()` | `Value` | Either lovelace-only or multi-asset | | `outputDatum()` | `OutputDatum` | NoOutputDatum, Hash, or InlineDatum | | `txOut()` | `TxOut` | Address + value + datum | | `txInInfo()` | `TxInInfo` | TxOutRef + TxOut | | `interval()` | `Interval` | always, never, after, before, or between | **PlutusData types:** | Generator | Type | Notes | |-----------|------|-------| | `intData()` | `PlutusData` | Integer [-1B, 1B] | | `intData(min, max)` | `PlutusData` | Bounded integer | | `bytesData()` | `PlutusData` | 0-64 byte array | | `bytesData(length)` | `PlutusData` | Fixed-length bytes | | `constrData(maxDepth)` | `PlutusData` | Random ConstrData | | `listData(maxDepth)` | `PlutusData` | Random ListData | | `mapData(maxDepth)` | `PlutusData` | Random MapData | | `plutusData()` | `PlutusData` | Any PlutusData (depth 3) | | `plutusData(maxDepth)` | `PlutusData` | Depth-bounded | ##### SPI Auto-Injection The `julc-testkit-jqwik` module registers a `CardanoArbitraryProvider` via Java SPI. This means jqwik can auto-inject Cardano types without `@Provide` methods: ```java // No @Provide needed — jqwik discovers the provider automatically @Property void test(@ForAll PubKeyHash pkh, @ForAll TxId txId) { // pkh and txId are auto-generated } ``` Supported auto-injected types: `PlutusData`, `PubKeyHash`, `ScriptHash`, `ValidatorHash`, `PolicyId`, `TokenName`, `DatumHash`, `TxId`, `Credential`, `Address`, `TxOutRef`, `Value`, `TxOut`, `TxInInfo`, `Interval`, `OutputDatum`. For custom constraints (e.g., bounded values), use `@Provide` with explicit generator methods. ##### BudgetCollector — Statistical Budget Analysis `BudgetCollector` aggregates budget data across many property test trials: ```java final BudgetCollector budgetCollector = new BudgetCollector(); @Property(tries = 500) void myProperty(@ForAll PubKeyHash pkh) { var result = ValidatorTest.evaluate(program, buildCtx(pkh)); BudgetAssertions.assertSuccess(result); budgetCollector.record(result); } @AfterProperty void reportBudget() { if (budgetCollector.count() > 0) { System.out.println(budgetCollector.summary()); // Output: // Budget (500 trials): CPU avg=12,345,678 min=11,000,000 max=14,000,000 p99=13,900,000 // Mem avg=45,678 min=40,000 max=52,000 p99=51,000 } } ``` | Method | Description | |--------|-------------| | `record(EvalResult)` | Record one trial's budget | | `count()` | Number of recorded trials | | `avgCpu()` / `avgMem()` | Average CPU/memory | | `maxCpu()` / `maxMem()` | Maximum CPU/memory | | `minCpu()` / `minMem()` | Minimum CPU/memory | | `p99Cpu()` / `p99Mem()` | 99th percentile | | `summary()` | Human-readable summary string | ##### ArbitraryScriptContext — Random Consistent Contexts `ArbitraryScriptContext` generates internally consistent ScriptContexts where the spent reference appears in inputs, signers appear in address credentials, etc. ```java import com.bloxbean.cardano.julc.testkit.jqwik.ArbitraryScriptContext; // Spending context with 1-3 signers, 2-5 inputs, 1-3 outputs Arbitrary ctxArb = ArbitraryScriptContext.spending() .signers(1, 3) .inputs(2, 5) .outputs(1, 3) .fee(150_000, 300_000) .withDatum(CardanoArbitraries.intData()) .withRedeemer(CardanoArbitraries.intData()) .withValidRange(CardanoArbitraries.interval()) .buildAsPlutusData(); // Minting context Arbitrary mintCtx = ArbitraryScriptContext.minting() .signers(1, 2) .inputs(1, 3) .build(); ``` ##### Custom Generators with @Provide For domain-specific constraints, write custom `@Provide` methods that compose `CardanoArbitraries` generators: ```java @Provide Arbitrary vestingDatum() { return Combinators.combine( CardanoArbitraries.pubKeyHash(), Arbitraries.bigIntegers().between(BigInteger.ZERO, BigInteger.valueOf(100_000))) .as((pkh, deadline) -> PlutusData.constr(0, PlutusData.bytes(pkh.hash()), PlutusData.integer(deadline))); } ``` ##### Property Design Tips **Positive properties** verify that authorized actions always succeed: - "The beneficiary can always unlock" - "The authorized minter can always mint" - "A valid bid always updates the auction state" **Negative properties** verify that unauthorized actions always fail: - "An attacker can never unlock" - "An unauthorized signer can never mint" - "A bid below the minimum always fails" **Budget properties** verify cost bounds: - "CPU never exceeds 50M steps" - "Memory never exceeds 200K units" **Structural properties** verify data integrity: - "Output value always equals input value minus fee" - "The continuing output always carries the updated datum" --- #### 8. Source Map Debugging Source maps map UPLC runtime errors back to Java source lines. Without them, a failure looks like `"Error term encountered"`. With them, you see `"Error at MyValidator.java:42 (Builtins.error())"`. ##### With JulcEval ```java // Enable source maps with one call var eval = JulcEval.forClass(MyValidator.class).sourceMap(); eval.call("validate", args); // Failures now include Java source location ``` ##### With ValidatorTest ```java // Compile with source maps CompileResult compiled = ValidatorTest.compileValidatorWithSourceMap(MyValidator.class); // Evaluate EvalResult result = ValidatorTest.evaluate(compiled.program(), ctx); // Resolve error location SourceLocation location = ValidatorTest.resolveErrorLocation(result, compiled.sourceMap()); System.out.println("Error at: " + location); // → "MyValidator.java:58 (Builtins.error())" // Or use assertion shorthand — throws with location in the message ValidatorTest.assertValidatesWithSourceMap(compiled, ctx); ``` ##### With ContractTest ```java class MyTest extends ContractTest { @Test void testWithSourceMap() { var compiled = compileValidatorWithSourceMap(MyValidator.class); var result = evaluate(compiled.program(), ctx); // Assert with source-mapped error messages assertSuccess(result, compiled.sourceMap()); // Or resolve the location manually var location = resolveErrorLocation(result, compiled.sourceMap()); // Log with budget and location logResult("myTest", result, compiled.sourceMap()); } } ``` ##### Execution Tracing Tracing provides a step-by-step view of which Java source lines executed and their CPU/memory cost: ```java var compiled = ValidatorTest.compileValidatorWithSourceMap(MyValidator.class); var evalTrace = ValidatorTest.evaluateWithTrace(compiled, ctx); System.out.println(evalTrace.formatTrace()); // MyValidator.java:30 var txInfo = ContextsLib.getTxInfo(ctx); CPU: 1,234 // MyValidator.java:31 var pkh = datum.beneficiary(); CPU: 567 // MyValidator.java:32 return ContextsLib.signedBy(txInfo, pkh); CPU: 8,901 System.out.println(evalTrace.formatBudgetSummary()); // Per-location budget breakdown ``` ##### Builtin Trace (Lightweight Failure Diagnostics) For quick failure diagnosis without execution tracing overhead, use **builtin trace** — it records the last 20 builtin executions and highlights the comparison that returned `False`. See [Builtin Trace](/guides/source-maps/#builtin-trace) in the Source Maps guide for full details. ```java // Lightweight: builtin trace only (no execution tracing overhead) var traced = ValidatorTest.evaluateWithBuiltinTrace(compiled, args); traced.builtinTrace(); // → List // Returns structured FailureReport on failure, null on success FailureReport report = ValidatorTest.evaluateWithBuiltinDiagnostics(compiled, args); if (report != null) System.out.println(FailureReportFormatter.format(report)); // Full diagnostics (execution trace + builtin trace) FailureReport report = ValidatorTest.evaluateWithDiagnostics(compiled, args); // Assert with rich error message on failure ValidatorTest.assertValidatesWithDiagnostics(compiled, args); ``` ##### Enabling Source Maps in Gradle For the annotation processor build pipeline: ```groovy julc { sourceMap = true } ``` Or via compiler options: ```groovy compileJava { options.compilerArgs += ['-Ajulc.sourceMap=true'] } ``` --- #### 9. Integration Testing with Yaci DevKit Integration tests submit real transactions to a local Cardano devnet. Use them for end-to-end verification: transaction building, script serialization, ledger rule validation, and fee calculation. ##### Prerequisites Yaci DevKit must be started externally before running integration tests: ```bash ### Start Yaci DevKit (separate terminal) yaci-devkit start ``` The DevKit exposes: - **Node API** on the default Cardano port - **Admin API** on port 10000 ##### Admin API — Reset and Fund ```bash ### Reset the devnet to genesis state curl -X POST http://localhost:10000/local-cluster/api/admin/devnet/reset ### Top up an address with test ADA curl -X POST http://localhost:10000/local-cluster/api/admin/topup \ -H 'Content-Type: application/json' \ -d '{"address": "addr_test1...", "adaAmount": 1000}' ``` Full OpenAPI documentation: `http://localhost:10000/v3/api-docs` ##### Integration Test Pattern ```java import com.bloxbean.cardano.client.api.model.Result; import com.bloxbean.cardano.client.backend.api.BackendService; class VestingIntegrationTest { static BackendService backendService; // from cardano-client-lib @BeforeAll static void setup() { // Connect to local Yaci DevKit backendService = new BFBackendService("http://localhost:10000/api/v1", ""); } @Test void deployAndUnlockVesting() { // 1. Compile validator var compiled = ValidatorTest.compileValidator(VestingValidator.class); var scriptHash = compiled.program().scriptHash(); // 2. Build lock transaction (send ADA to script address) // ... use cardano-client-lib transaction builder ... // 3. Submit lock tx Result lockResult = transactionService.submitTransaction(signedLockTx); assertTrue(lockResult.isSuccessful()); // 4. Build unlock transaction (claim from script) // ... attach compiled script, datum, redeemer ... // 5. Submit unlock tx Result unlockResult = transactionService.submitTransaction(signedUnlockTx); assertTrue(unlockResult.isSuccessful()); } } ``` > Integration tests are slower and require external infrastructure. Use UPLC > evaluation tests for the bulk of your testing, and integration tests for > final verification before deployment. --- #### 10. Testing Patterns and Best Practices ##### Compile-Once, Evaluate-Many Compilation is the expensive step. Always compile once and reuse the `Program`: ```java // Good — compile once as a static field static final Program VALIDATOR = ValidatorTest.compileValidator(MyValidator.class).program(); @Test void test1() { ValidatorTest.evaluate(VALIDATOR, ctx1); } @Test void test2() { ValidatorTest.evaluate(VALIDATOR, ctx2); } // Bad — recompiles on every test @Test void test1() { ValidatorTest.evaluate(ValidatorTest.compileValidator(MyValidator.class).program(), ctx1); } ``` ##### Testing Sealed Interface Redeemers When your validator uses a sealed interface for the redeemer, test every variant: ```java // Validator with action redeemer: // sealed interface Action { record Mint() implements Action {} record Burn() implements Action {} } @Test void mintActionAccepted() { var redeemer = PlutusData.constr(0); // Mint = Constr(0, []) var ctx = buildMintCtx(redeemer); BudgetAssertions.assertSuccess(ValidatorTest.evaluate(program, ctx)); } @Test void burnActionAccepted() { var redeemer = PlutusData.constr(1); // Burn = Constr(1, []) var ctx = buildBurnCtx(redeemer); BudgetAssertions.assertSuccess(ValidatorTest.evaluate(program, ctx)); } @Test void invalidActionRejected() { var redeemer = PlutusData.constr(99); // Unknown variant var ctx = buildMintCtx(redeemer); BudgetAssertions.assertFailure(ValidatorTest.evaluate(program, ctx)); } ``` ##### Testing @Param Validators Parameterized validators take compile-time parameters applied before deployment. Use `program.applyParams()` to bind them in tests: ```java static final Program BASE = ValidatorTest.compileValidator(MyParamValidator.class).program(); @Test void parameterizedValidator() { // Apply parameters: owner PKH and minimum ADA var parameterized = BASE.applyParams( PlutusData.bytes(ownerPkh), PlutusData.integer(2_000_000)); var ctx = buildCtx(ownerPkh); BudgetAssertions.assertSuccess(ValidatorTest.evaluate(parameterized, ctx)); } ``` ##### Testing Trace Output Use `Builtins.trace()` in your validator for debugging, then verify traces in tests: ```java @Test void validatorTracesExpectedMessages() { var result = ValidatorTest.evaluate(program, ctx); BudgetAssertions.assertSuccess(result); BudgetAssertions.assertTrace(result, "checking signature", "signature valid"); } @Test void failureTraceExplainsReason() { var result = ValidatorTest.evaluate(program, badCtx); BudgetAssertions.assertFailure(result); BudgetAssertions.assertTrace(result, "signature check failed"); } ``` ##### Multi-File Compilation When your validator depends on helper classes, `compileValidator(Class)` auto-discovers `@OnchainLibrary` dependencies automatically. For helper classes without the `@OnchainLibrary` annotation, use the inline `compile()` with multiple sources: ```java static final String MATH_LIB = """ class MathUtils { static BigInteger max(BigInteger a, BigInteger b) { if (a > b) { return a; } else { return b; } } } """; static final String VALIDATOR = """ @SpendingValidator class AuctionValidator { @Entrypoint static boolean validate(BigInteger redeemer, BigInteger ctx) { return MathUtils.max(100, 50) == 100; } } """; @Test void multiFileCompilation() { // ContractTest.compile() accepts additional library sources var program = compile(VALIDATOR, MATH_LIB); assertValidates(program, PlutusData.integer(0), PlutusData.integer(0)); } ``` > In production code, prefer annotating shared helper classes with > `@OnchainLibrary` so `compileValidator()` picks them up automatically. ##### Direct Java Tests for Stdlib Test standard library calls directly as Java — useful for verifying business logic without compiling to UPLC: ```java @Nested class StdlibDirectTests { @Test void valueOperationsWork() { var value = Value.lovelace(BigInteger.valueOf(10_000_000)); assertEquals(BigInteger.valueOf(10_000_000), value.lovelaceOf()); } @Test void intervalContainmentWorks() { var interval = Interval.between(BigInteger.valueOf(1000), BigInteger.valueOf(2000)); assertTrue(IntervalLib.contains(interval, BigInteger.valueOf(1500))); assertFalse(IntervalLib.contains(interval, BigInteger.valueOf(500))); } } ``` --- #### 11. Quick Reference ##### Which Tool for Which Scenario | Scenario | Tool | Base class | |----------|------|------------| | Test a helper method (math, logic, string) | `JulcEval` | None needed | | Test validator with datum + redeemer + ctx | `ValidatorTest` or `ContractTest` | Optional | | Debug validator logic with IDE breakpoints | Direct Java calls | `ContractTest` | | Budget regression testing | `BudgetAssertions` | None needed | | Script size verification | `ScriptAnalysis` | None needed | | Fuzz with random inputs (100+ trials) | jqwik + `CardanoArbitraries` | None needed | | Budget statistics across many trials | `BudgetCollector` | None needed | | Source-mapped error messages | `.sourceMap()` / `compileWithSourceMap()` | Either | | End-to-end with real transactions | Yaci DevKit + cardano-client-lib | None needed | ##### Module Dependencies | Module | Provides | |--------|----------| | `julc-testkit` | `ContractTest`, `ValidatorTest`, `ScriptContextTestBuilder`, `BudgetAssertions`, `TestDataBuilder`, `JulcEval`, `ScriptAnalysis` | | `julc-testkit-jqwik` | `CardanoArbitraries`, `ArbitraryScriptContext`, `BudgetCollector`, `CardanoArbitraryProvider` (SPI) | | `julc-vm-scalus` | Scalus-based CEK machine (runtime dependency) | ##### Minimal Test Setup (Gradle) ```groovy dependencies { testImplementation 'com.bloxbean.cardano:julc-testkit:' testRuntimeOnly 'com.bloxbean.cardano:julc-vm-scalus:' } test { useJUnitPlatform() } ``` ##### Minimal Test with Property Testing (Gradle) ```groovy dependencies { testImplementation 'com.bloxbean.cardano:julc-testkit:' testImplementation 'com.bloxbean.cardano:julc-testkit-jqwik:' testImplementation 'net.jqwik:jqwik:1.9.2' testRuntimeOnly 'com.bloxbean.cardano:julc-vm-scalus:' } test { useJUnitPlatform() } ``` --- ## Source Maps — Runtime Error Location Reporting Source: https://julc.dev/guides/source-maps/ > Source Maps — Runtime Error Location Reporting - JuLC documentation When a UPLC program fails at runtime (Error term, budget exhaustion, builtin type error), the default error message points to opaque UPLC internals: ``` Evaluation failed: Error term encountered ``` Source maps bridge this gap by mapping UPLC terms back to the Java source line that generated them. #### Quick Start ##### With `JulcEval` (recommended) Add `.sourceMap()` to your evaluator: ```java // Before (no source location) var eval = JulcEval.forClass(SwapOrder.class); PlutusData result = eval.call("makerAddress", MAKER).asData(); // On failure: "Evaluation failed: Error term encountered" // After (with source location) var eval = JulcEval.forClass(SwapOrder.class).sourceMap(); PlutusData result = eval.call("makerAddress", MAKER).asData(); // On failure: "Evaluation failed: Error term encountered // at SwapOrder.java:42 (Builtins.error())" ``` That's it. One method call. The error message now includes the file name, line number, and a snippet of the Java expression that caused the failure. ##### With `ValidatorTest` For validator-level testing: ```java // Compile with source maps CompileResult compiled = ValidatorTest.compileValidatorWithSourceMap(EscrowValidator.class); // Evaluate EvalResult result = ValidatorTest.evaluate(compiled.program(), scriptContext); // On failure, resolve the Java source location SourceLocation location = ValidatorTest.resolveErrorLocation(result, compiled.sourceMap()); System.out.println("Error at: " + location); // → "EscrowValidator.java:58 (Builtins.error())" ``` Or use the assertion shorthand: ```java // Throws with source location in the error message ValidatorTest.assertValidatesWithSourceMap(compiled, scriptContext); // → AssertionError: Expected validator to succeed, but got: // Failure{error=Error term encountered, budget=ExBudget{cpu=..., mem=...}, traces=[]} // at EscrowValidator.java:58 (Builtins.error()) ``` --- #### Enabling Source Maps Source maps must be enabled at compile time. There are three ways depending on your build setup. ##### Gradle Plugin DSL If you use the `julc` Gradle plugin: ```groovy julc { sourceMap = true } ``` ##### Annotation Processor in Gradle (without julc plugin) Pass the `-Ajulc.sourceMap=true` compiler arg directly: ```groovy tasks.withType(JavaCompile).configureEach { options.compilerArgs.add('-Ajulc.sourceMap=true') } ``` ##### Annotation Processor in Maven ```xml org.apache.maven.plugins maven-compiler-plugin -Ajulc.sourceMap=true ``` In all cases, the annotation processor reads the option via `processingEnv.getOptions().get("julc.sourceMap")` and writes a `.sourcemap.json` file alongside the compiled `.plutus.json` under `META-INF/plutus/`. --- #### Usage Patterns ##### Pattern 1: JulcEval with `.sourceMap()` (simplest) ```java @Test void testSwapOrder() { var eval = JulcEval.forClass(SwapOrder.class).sourceMap(); // Happy path — works as before PlutusData result = eval.call("makerAddress", MAKER).asData(); assertNotNull(result); // Error path — now shows source location in the exception message assertThrows(ExtractionException.class, () -> eval.call("validate", invalidData).asBoolean() ); } ``` ##### Pattern 2: Validator compilation with source maps ```java @Test void testEscrowValidator() { // Compile once with source maps var compiled = ValidatorTest.compileValidatorWithSourceMap(EscrowValidator.class); // Evaluate multiple scenarios var ctx1 = buildScriptContext(/* valid */); ValidatorTest.assertValidatesWithSourceMap(compiled, ctx1); var ctx2 = buildScriptContext(/* invalid */); ValidatorTest.assertRejectsWithSourceMap(compiled, ctx2); } ``` ##### With `ContractTest` For real project tests (e.g., julc-examples) that extend `ContractTest`: ```java class SwapOrderTest extends ContractTest { @Test void cancel_rejectsNonMaker() throws Exception { // Compile with source maps (drop-in replacement for compileValidator) var compiled = compileValidatorWithSourceMap(SwapOrder.class); var ref = TestDataBuilder.randomTxOutRef_typed(); var ctx = spendingContext(ref, datum) .redeemer(redeemer) .signer(OTHER_PKH) .buildPlutusData(); var result = evaluate(compiled.program(), ctx); // assertFailure with source map — error message includes Java source location assertFailure(result, compiled.sourceMap()); // Optional: log budget + error location logResult("cancel_rejectsNonMaker", result, compiled.sourceMap()); // → [cancel_rejectsNonMaker] CPU: 967522, Mem: 4536 | Error at: SwapOrder.java:42 (Builtins.error()) // Optional: resolve location programmatically var location = resolveErrorLocation(result, compiled.sourceMap()); System.out.println("Error at: " + location); } } ``` Available `ContractTest` source map methods: - `compileValidatorWithSourceMap(Class)` — compile with source maps (default source root) - `compileValidatorWithSourceMap(Class, Path)` — compile with custom source root - `assertSuccess(result, sourceMap)` / `assertFailure(result, sourceMap)` — assertions with source location in error messages - `resolveErrorLocation(result, sourceMap)` — get `SourceLocation` programmatically - `logResult(testName, result, sourceMap)` — print budget + error location ##### Pattern 3: Programmatic (full control) ```java @Test void testWithFullControl() { var options = new CompilerOptions().setSourceMapEnabled(true); var compiler = new JulcCompiler(StdlibRegistry.defaultRegistry(), options); CompileResult compiled = compiler.compile(source); SourceMap sourceMap = compiled.sourceMap(); var vm = JulcVm.create(); EvalResult result = vm.evaluateWithArgs(compiled.program(), List.of(scriptContext)); if (!result.isSuccess()) { // Get the failed Term from the result Term failedTerm = switch (result) { case EvalResult.Failure f -> f.failedTerm(); case EvalResult.BudgetExhausted b -> b.failedTerm(); default -> null; }; // Look up in source map SourceLocation location = sourceMap.lookup(failedTerm); if (location != null) { System.out.println("Error at: " + location); // → "MyValidator.java:42 (Builtins.error())" } } } ``` --- #### Execution Tracing Source maps tell you *where* a failure occurred. Execution tracing goes further — it records **every CEK machine step** that has a source mapping, along with the CPU and memory consumed at each step. This lets you see the hot path through your validator and identify which Java lines consume the most budget. ##### What a trace contains Each trace entry is an `ExecutionTraceEntry` record: ```java public record ExecutionTraceEntry( String fileName, // Java source file name int line, // 1-based line number String fragment, // Java expression snippet (nullable) String nodeType, // CEK step type: "Apply", "Force", "Case", "Error" long cpuDelta, // CPU consumed since previous trace point long memDelta // Memory consumed since previous trace point ) ``` ##### With ValidatorTest (static) ```java var compiled = ValidatorTest.compileValidatorWithSourceMap(MyValidator.class); var traced = ValidatorTest.evaluateWithTrace(compiled, scriptContext); // Step-by-step trace with IntelliJ-clickable file:line links System.out.println(traced.formatTrace()); // Aggregated CPU/mem by source location, sorted by cost System.out.println(traced.formatBudgetSummary()); // Access the raw result EvalResult result = traced.result(); ``` ##### With ContractTest (instance) ```java class MyTest extends ContractTest { @Test void testBudget() { var compiled = compileValidatorWithSourceMap(MyValidator.class); var result = evaluateWithTrace(compiled, ctx); // Format last trace System.out.println(formatExecutionTrace()); // Aggregated budget summary System.out.println(formatBudgetSummary()); // Or access raw entries List entries = getLastExecutionTrace(); } } ``` ##### Output format: `format()` (step-by-step) ``` Execution trace (1247 steps): at .(MyValidator.java:18) Apply cpu=+230 mem=+100 | validate(PlutusData, PlutusData) at .(MyValidator.java:22) Apply cpu=+456 mem=+200 | txInfo.fee() at .(MyValidator.java:23) Force cpu=+128 mem=+64 | list.head() ... Total: cpu=967522, mem=4536 ``` File and line references use `at .(File.java:line)` format, which is clickable in IntelliJ console output. ##### Output format: `formatSummary()` (aggregated by location) ``` Budget by source location (top consumers first): MyValidator.java:42 visits=12 cpu=245000 mem=1200 | Builtins.equalsData(a, b) MyValidator.java:38 visits=8 cpu=180000 mem=900 | list.head() MyValidator.java:55 visits=1 cpu=95000 mem=450 | Builtins.verifyEd25519Signature(...) ... ``` This format groups all visits to the same source line, sums their budgets, and sorts by CPU cost descending — making it easy to find optimization targets. --- #### Builtin Trace Builtin trace records the **last 20 builtin function executions** in a ring buffer — function name, arguments, and result. It answers **"what values caused the failure?"**, complementary to source maps ("where?") and execution tracing ("what path?"). Key properties: - **Always on** by default — no compile-time or runtime flags needed - **Negligible overhead** — ring buffer of 20 entries, lazy string formatting (hot path stores raw refs) - **No source map required** — useful for pre-compiled `.plutus` files in the CLI - **SPI-agnostic** — works with any VM backend (uses string summaries, not impl-specific types) ##### Comparison | Aspect | Source Map | Builtin Trace | Execution Trace | |--------|-----------|---------------|-----------------| | Answers | Where did it fail? | What values caused it? | What path did execution take? | | Overhead | Compile-time only | Negligible (ring buffer) | Heavy (per-step recording) | | Opt-in | Compile with sourceMap=true | **Always on** (default) | Enable tracing + source map | | Output | `MyValidator.java:42` | `EqualsInteger(5, 3) → False` | Full step-by-step trace | | Use case | Locate error in source | Quick failure diagnosis | Budget profiling | ##### Usage from each entry point ###### JulcEval (fluent API) ```java var eval = JulcEval.forClass(MyValidator.class).builtinTrace(); eval.call("validate", data).asBoolean(); // On failure: rich FailureReport in exception message // After any call: List trace = eval.getLastBuiltinTrace(); ``` > **Note:** `.builtinTrace()` is an alias for `.sourceMap()` — it communicates intent. Builtin > trace is always collected regardless. ###### ValidatorTest (static API) ```java // Lightweight: builtin trace only (no execution tracing overhead) var traced = ValidatorTest.evaluateWithBuiltinTrace(compiled, args); traced.builtinTrace(); // → List // Lightweight diagnostics: returns FailureReport on failure FailureReport report = ValidatorTest.evaluateWithBuiltinDiagnostics(compiled, args); if (report != null) System.out.println(FailureReportFormatter.format(report)); // Full diagnostics: both execution trace + builtin trace FailureReport report = ValidatorTest.evaluateWithDiagnostics(compiled, args); // Assert with rich error message on failure ValidatorTest.assertValidatesWithDiagnostics(compiled, args); ``` ###### ContractTest (instance methods) ```java class MyTest extends ContractTest { @Test void test() { var compiled = compileValidatorWithSourceMap(MyValidator.class); // Lightweight eval (no execution tracing) var result = evaluateWithBuiltinTrace(compiled, ctx); // Access builtin trace from shared VM List trace = getLastBuiltinTrace(); for (var exec : trace) { System.out.println(exec); // EqualsInteger(5, 3) → False } } } ``` ###### CLI (`julc eval`) ```bash $ julc eval my-validator.plutus FAIL: Error term encountered EqualsInteger(5, 3) → False Last builtins: UnIData() → 5 UnIData() → 3 EqualsInteger(5, 3) → False Budget: CPU=1,234,567 Mem=45,678 ``` The CLI automatically collects builtin trace and uses `AnsiFailureReportFormatter` for colored terminal output. ###### Programmatic (JulcVm directly) ```java var vm = JulcVm.create(); // Per-evaluation options — thread-safe, no shared state var options = EvalOptions.DEFAULT .withSourceMap(sourceMap) .withTracing(true); EvalResult result = vm.evaluateWithArgs(program, List.of(args), options); // Traces are in the result List trace = result.builtinTrace(); List execTrace = result.executionTrace(); // Disable builtin trace for zero-overhead production eval var prodOptions = EvalOptions.DEFAULT.withBuiltinTrace(false); EvalResult result2 = vm.evaluate(program, prodOptions); ``` ##### Advantages - Zero configuration — always collected, no compile-time or runtime flags needed - Negligible overhead — ring buffer of 20 entries, lazy string formatting - Pinpoints the **exact comparison that failed** — `findCauseBuiltin()` scans backwards for False-returning comparisons (`EqualsInteger`, `LessThanInteger`, `EqualsByteString`, etc.) - Works without source maps — useful for pre-compiled `.plutus` files - SPI-agnostic — works with any VM backend ##### Limitations - Only last 20 builtins — earlier operations are lost (ring buffer) - No source location per builtin — builtins aren't mapped to Java lines (use source map for that) - Values are summarized — ByteStrings truncated to 16 hex chars, strings to 20 chars, complex values shown as ``, `[N elems]`, `` - Cannot be disabled per-call from testkit APIs (always collected if VM supports it; disable via `JulcVm.setBuiltinTraceEnabled(false)`) --- #### Failure Reports `FailureReport` is a structured record that bundles all diagnostic context for a failed evaluation: error message, source location, last builtins, execution trace, budget, and trace messages. ##### Building a FailureReport `FailureReportBuilder.build()` constructs a report from an evaluation result and optional traces: ```java FailureReport report = FailureReportBuilder.build(result, sourceMap, executionTrace, builtinTrace); // Convenience overloads: FailureReport report = FailureReportBuilder.build(result, sourceMap); // no traces FailureReport report = FailureReportBuilder.build(result); // no source map or traces ``` Returns `null` if the result is a `Success`. ##### Formatting `FailureReportFormatter.format(report)` produces plain text output: ``` FAIL at .(VestingValidator.java:42) return deadline <= currentSlot LessThanEqualsInteger(5, 3) → False Last builtins: UnIData() → 5 UnIData() → 3 LessThanEqualsInteger(5, 3) → False Budget: CPU=1,234,567 Mem=45,678 ``` The header shows the source location (if available) or the error message. The highlighted "cause" line is the last comparison builtin that returned `False`, identified by `report.findCauseBuiltin()`. For CLI output, `AnsiFailureReportFormatter` adds ANSI colors: `FAIL` in red, builtin names in cyan, `→ False` in red, `→ True` in green, budget in dim. --- #### Choosing the Right Diagnostic Level From lightest to heaviest: 1. **No diagnostics** — `ValidatorTest.evaluate()` / `ContractTest.evaluate()` — fastest, just success/failure + budget 2. **Builtin trace only** — `evaluateWithBuiltinTrace()` — adds "what values failed?" at negligible cost 3. **Builtin diagnostics** — `evaluateWithBuiltinDiagnostics()` — returns structured `FailureReport` with source location + builtins 4. **Full diagnostics** — `evaluateWithDiagnostics()` — adds per-step execution trace for budget profiling > **Note:** Builtin trace is always collected by the VM (enabled by default). The difference > between levels 1 and 2 is whether you *retrieve* the trace, not whether it's *recorded*. > For zero-overhead production evaluation, disable with `vm.setBuiltinTraceEnabled(false)`. --- #### Using with QuickTxBuilder (JulcTransactionEvaluator) For offchain integration with cardano-client-lib's `QuickTxBuilder`, use `JulcTransactionEvaluator` with source maps and tracing. ##### Loading scripts with source maps `JulcScriptLoader.loadWithSourceMap()` loads a pre-compiled script from the classpath and reconstructs its source map with correct Term object identity: ```java // Load script + source map + deserialized program JulcScriptLoader.LoadResult loaded = JulcScriptLoader.loadWithSourceMap(MyValidator.class); PlutusV3Script script = loaded.script(); // ready for QuickTxBuilder SourceMap sourceMap = loaded.sourceMap(); // may be null if no .sourcemap.json Program program = loaded.program(); // deserialized UPLC program // For parameterized validators var loaded = JulcScriptLoader.loadWithSourceMap(MyValidator.class, param1, param2); ``` `LoadResult` is a record: ```java public record LoadResult( PlutusV3Script script, SourceMap sourceMap, Program program ) ``` ##### Registering scripts and enabling tracing ```java // Create the evaluator (typically with a BackendService for protocol params) var evaluator = new JulcTransactionEvaluator(backendService); // Register script so the evaluator can resolve it by hash evaluator.registerScript(script.getScriptHash(), loaded); // Enable per-step execution tracing evaluator.enableTracing(true); // Use with QuickTxBuilder var quickTx = new QuickTxBuilder(backendService) .withTxEvaluator(evaluator); ``` ##### Retrieving traces after evaluation ```java // After QuickTxBuilder submits a transaction... Map> traces = evaluator.getLastTraces(); // Traces are keyed by redeemer tag+index, e.g. "SPEND[0]", "MINT[0]" for (var entry : traces.entrySet()) { System.out.println("=== " + entry.getKey() + " ==="); System.out.println(ExecutionTraceEntry.format(entry.getValue())); System.out.println(ExecutionTraceEntry.formatSummary(entry.getValue())); } ``` **Important**: When tracing is enabled, traces are automatically printed to stdout during evaluation. This is intentional — `QuickTxBuilder` may swallow `Result.error()` messages, so printing ensures you always see the trace output even if the transaction fails silently. ##### Source map methods You can also register source maps and programs individually: ```java evaluator.setSourceMap(scriptHash, sourceMap); evaluator.setProgram(scriptHash, program); ``` --- #### What Gets Mapped Source maps capture positions for these Java constructs: | Java Construct | Example | Mapped? | |---------------|---------|---------| | Method calls | `Builtins.error()`, `list.head()` | Yes | | Binary expressions | `a.compareTo(b) < 0` | Yes | | Unary expressions | `!isValid` | Yes | | Field access | `txInfo.fee()` | Yes | | Object creation | `new EscrowDatum(...)` | Yes | | If statements | `if (amount < 0) { ... }` | Yes | | Switch expressions | `switch (action) { ... }` | Yes | | Return statements | `return result` | Yes | | Method definitions | `public static boolean validate(...)` | Yes | | Conditionals | `cond ? a : b` | Yes | | Literals | `42`, `true`, `"hello"` | No (not useful) | | Variable references | `amount` | No (not useful) | | Compiler-generated code | ValidatorWrapper, wrapDecode | No (no Java source) | --- #### How It Works ##### Architecture ``` Java Source → PirGenerator → PIR Terms → UplcGenerator → UPLC Terms → CekMachine ↓ ↓ ↓ pirPositions map uplcPositions map currentTerm (PirTerm → SourceLocation) (Term → SourceLocation) (on exception) ↓ SourceMap (IdentityHashMap) ``` 1. **PirGenerator** records `SourceLocation` for each PIR term it creates from a JavaParser AST node 2. **UplcGenerator** transfers positions from PIR terms to their outermost UPLC terms, propagating parent locations to children 3. The resulting `IdentityHashMap` is wrapped in a `SourceMap` 4. When the **CekMachine** throws an exception, it attaches `currentTerm` (the UPLC term being evaluated) 5. **JavaVmProvider** passes `failedTerm` through to `EvalResult.Failure`/`BudgetExhausted` 6. The testkit resolves `failedTerm` against the `SourceMap` to get the `SourceLocation` ##### Why optimization is skipped Source maps use `IdentityHashMap` — lookups are by object identity (`==`), not structural equality. The UPLC optimizer creates new Term objects, which breaks identity. Since source maps are a debugging feature, skipping optimization is the right trade-off: unoptimized UPLC is functionally identical, just uses more budget. ##### Scalus VM backend Source maps only work with the **Java VM backend** (`julc-vm-java`). The Scalus backend serializes terms to FLAT format and re-parses them, which destroys object identity. When using Scalus, `failedTerm` will be null. --- #### API Reference ##### `CompilerOptions` ```java new CompilerOptions() .setSourceMapEnabled(true) // enable source map generation .setVerbose(true); // optional: log source map stats ``` ##### `SourceLocation` ```java record SourceLocation(String fileName, int line, int column, String fragment) // toString() → "MyValidator.java:42 (Builtins.error())" ``` ##### `SourceMap` ```java SourceMap.EMPTY // always returns null sourceMap.lookup(term) // → SourceLocation or null sourceMap.size() // number of mapped terms sourceMap.isEmpty() // true if no entries ``` ##### `EvalResult.Failure` / `EvalResult.BudgetExhausted` ```java failure.failedTerm() // → Term (nullable) — the UPLC term that caused the error exhausted.failedTerm() // → Term (nullable) — the UPLC term when budget ran out ``` ##### `ExecutionTraceEntry` ```java record ExecutionTraceEntry( String fileName, int line, String fragment, String nodeType, long cpuDelta, long memDelta ) // Format a list of entries as step-by-step trace ExecutionTraceEntry.format(entries) // → String (multi-line, with totals) // Aggregate by source location, sorted by CPU cost ExecutionTraceEntry.formatSummary(entries) // → String (multi-line, with visit counts) // toString() uses IntelliJ-clickable format: "at .(File.java:42)" ``` ##### `BuiltinExecution` ```java record BuiltinExecution(DefaultFun fun, String argSummary, String resultSummary) // toString() → "EqualsInteger(5, 3) → False" ``` Values are summarized: ByteStrings truncated to 16 hex chars (`#a1b2c3...`), strings to 20 chars, complex values shown as ``, `[N elems]`, ``. ##### `FailureReport` ```java record FailureReport( String errorMessage, // VM error message SourceLocation sourceLocation, // Java source location (nullable) List lastBuiltins, // last N builtin executions List lastSteps, // last N execution trace entries ExBudget consumed, // budget consumed List traceMessages // Builtins.trace() messages ) report.findCauseBuiltin() // → last comparison builtin returning False, or null ``` ##### `FailureReportBuilder` ```java FailureReportBuilder.build(result, sourceMap, executionTrace, builtinTrace) // → FailureReport or null FailureReportBuilder.build(result, sourceMap) // no traces FailureReportBuilder.build(result) // no source map or traces ``` ##### `FailureReportFormatter` ```java FailureReportFormatter.format(report) // → plain text multi-line string ``` ##### `EvalResult` (traces) ```java result.builtinTrace() // → List (empty if disabled) result.executionTrace() // → List (empty if tracing off) ``` ##### `EvalOptions` (per-evaluation configuration) ```java EvalOptions.DEFAULT // no source map, no tracing, builtin trace ON EvalOptions.DEFAULT.withSourceMap(sourceMap) // enable source maps EvalOptions.DEFAULT.withTracing(true) // enable execution tracing EvalOptions.DEFAULT.withBuiltinTrace(false) // disable builtin trace (zero-overhead) new EvalOptions(sourceMap, true, true) // all three at once ``` ##### `ValidatorTest` ```java // Compile with source maps ValidatorTest.compileValidatorWithSourceMap(MyValidator.class) ValidatorTest.compileValidatorWithSourceMap(MyValidator.class, sourceRoot) ValidatorTest.compileWithSourceMap(javaSource) // Evaluate with execution tracing ValidatorTest.evaluateWithTrace(compiled, args...) // → EvalResult (with traces) // Evaluate with builtin trace only (lightweight, no execution tracing) ValidatorTest.evaluateWithBuiltinTrace(compiled, args...) // → EvalResult (with builtin trace) // Diagnostics: builtin-only (lightweight) or full (with execution trace) ValidatorTest.evaluateWithBuiltinDiagnostics(compiled, args...) // → FailureReport or null ValidatorTest.evaluateWithDiagnostics(compiled, args...) // → FailureReport or null // Resolve error location ValidatorTest.resolveErrorLocation(result, sourceMap) // → SourceLocation or null // Assert with source location in error message ValidatorTest.assertValidatesWithSourceMap(compileResult, args...) ValidatorTest.assertRejectsWithSourceMap(compileResult, args...) // Assert with rich diagnostics (FailureReport) on failure ValidatorTest.assertValidatesWithDiagnostics(compiled, args...) ``` ##### `ContractTest` ```java // Compile with source maps compileValidatorWithSourceMap(MyValidator.class) compileValidatorWithSourceMap(MyValidator.class, sourceRoot) // Evaluate with tracing evaluateWithTrace(compiled, args...) // → EvalResult (trace stored internally) // Evaluate with builtin trace only (no execution tracing) evaluateWithBuiltinTrace(compiled, args...) // → EvalResult (trace stored internally) // Access last traces getLastExecutionTrace() // → List getLastBuiltinTrace() // → List formatExecutionTrace() // → formatted step-by-step trace formatBudgetSummary() // → formatted per-location budget summary // Assertions with source location in error messages assertSuccess(result, sourceMap) assertFailure(result, sourceMap) // Resolve error location resolveErrorLocation(result, sourceMap) // → SourceLocation or null // Log budget + error location logResult("testName", result, sourceMap) ``` ##### `JulcEval` ```java JulcEval.forClass(MyClass.class).sourceMap() // enable source maps JulcEval.forClass(MyClass.class).builtinTrace() // alias for sourceMap() — communicates intent JulcEval.forClass(MyClass.class).trace() // enable execution tracing (implies sourceMap) JulcEval.forSource(javaString).sourceMap() // works with inline source too eval.getLastBuiltinTrace() // → List eval.getLastExecutionTrace() // → List eval.formatLastTrace() // → formatted step-by-step trace eval.formatLastBudgetSummary() // → formatted per-location budget summary ``` ##### `JulcScriptLoader` ```java // Load pre-compiled script with source map from classpath JulcScriptLoader.loadWithSourceMap(MyValidator.class) // → LoadResult JulcScriptLoader.loadWithSourceMap(MyValidator.class, params...)// → LoadResult (parameterized) JulcScriptLoader.loadSourceMap(MyValidator.class) // → SourceMap (nullable) ``` ##### `JulcScriptLoader.LoadResult` ```java record LoadResult(PlutusV3Script script, SourceMap sourceMap, Program program) loaded.script() // PlutusV3Script for QuickTxBuilder loaded.sourceMap() // SourceMap (null if no .sourcemap.json) loaded.program() // deserialized UPLC Program ``` ##### `JulcTransactionEvaluator` ```java evaluator.registerScript(scriptHash, loadResult) // register script + source map + program evaluator.setSourceMap(scriptHash, sourceMap) // register source map only evaluator.setProgram(scriptHash, program) // register program only evaluator.enableTracing(true) // enable per-step tracing evaluator.getLastTraces() // → Map> // keyed by redeemer tag+index, e.g. "SPEND[0]" ``` --- #### Limitations - **Compiler-generated terms** (ValidatorWrapper lambdas, wrapDecode/wrapEncode, Z-combinator) have no source position — `lookup()` returns null. - **Library method errors**: The source map points to the *call site* in user code, not the library implementation. This is intentional. - **String sources**: When compiling from a string (not a file), `fileName` is null. The location shows `:42 (fragment)` instead of `File.java:42 (fragment)`. - **Optimization disabled**: Source maps skip UPLC optimization. Budget numbers will be higher than production. Use source maps for debugging, not benchmarking. - **Java VM only**: The Scalus VM backend does not support source maps. --- # Standard Library --- ## JuLC Standard Library Usage Guide Source: https://julc.dev/stdlib/stdlib-guide/ > JuLC Standard Library Usage Guide - JuLC documentation The JuLC standard library provides 13 on-chain libraries in the `com.bloxbean.cardano.julc.stdlib.lib` package. Each library is annotated with `@OnchainLibrary` and compiled from Java source to UPLC. All methods are `static` and can be called directly from your validator code. #### Overview | Library | Import Path | Purpose | |---------|-------------|---------| | **ContextsLib** | `com.bloxbean.cardano.julc.stdlib.lib.ContextsLib` | Script context, TxInfo field access, signatory checks, datum lookup | | **ListsLib** | `com.bloxbean.cardano.julc.stdlib.lib.ListsLib` | List construction, traversal, search, and higher-order functions | | **ValuesLib** | `com.bloxbean.cardano.julc.stdlib.lib.ValuesLib` | Multi-asset Value comparison, arithmetic, and extraction | | **MapLib** | `com.bloxbean.cardano.julc.stdlib.lib.MapLib` | Association list (map) lookup, insert, delete, keys/values | | **OutputLib** | `com.bloxbean.cardano.julc.stdlib.lib.OutputLib` | Output filtering by address/token, lovelace summation, datum extraction | | **MathLib** | `com.bloxbean.cardano.julc.stdlib.lib.MathLib` | abs, max, min, pow, divMod, quotRem, expMod, sign | | **IntervalLib** | `com.bloxbean.cardano.julc.stdlib.lib.IntervalLib` | Time interval construction, containment, bound extraction | | **CryptoLib** | `com.bloxbean.cardano.julc.stdlib.lib.CryptoLib` | Hash functions and signature verification | | **ByteStringLib** | `com.bloxbean.cardano.julc.stdlib.lib.ByteStringLib` | ByteString slicing, comparison, encoding, serialization | | **BitwiseLib** | `com.bloxbean.cardano.julc.stdlib.lib.BitwiseLib` | Bitwise AND/OR/XOR, shift, rotate, bit read/write | | **AddressLib** | `com.bloxbean.cardano.julc.stdlib.lib.AddressLib` | Credential extraction, address type checks | | **BlsLib** | `com.bloxbean.cardano.julc.stdlib.lib.BlsLib` | BLS12-381 G1/G2/pairing/MSM operations | | **NativeValueLib** *(PV11)* | `com.bloxbean.cardano.julc.stdlib.lib.NativeValueLib` | Native MaryEra Value insert, lookup, union, contains, scale | --- #### Quick Reference ##### ContextsLib | Method | Description | |--------|-------------| | `signedBy(txInfo, pkh)` | Check if a PubKeyHash is in the signatories | | `getTxInfo(ctx)` | Extract TxInfo from ScriptContext (legacy; prefer `ctx.txInfo()`) | | `getRedeemer(ctx)` | Extract redeemer from ScriptContext | | `getSpendingDatum(ctx)` | Extract optional spending datum | | `txInfoInputs(txInfo)` | Get list of inputs | | `txInfoOutputs(txInfo)` | Get list of outputs | | `txInfoSignatories(txInfo)` | Get signatories list | | `txInfoValidRange(txInfo)` | Get valid time range | | `txInfoMint(txInfo)` | Get minted value | | `txInfoFee(txInfo)` | Get transaction fee | | `txInfoId(txInfo)` | Get transaction ID | | `txInfoRefInputs(txInfo)` | Get reference inputs | | `txInfoWithdrawals(txInfo)` | Get withdrawals map | | `txInfoRedeemers(txInfo)` | Get redeemers map | | `findOwnInput(ctx)` | Find the input being validated | | `getContinuingOutputs(ctx)` | Get outputs to the same script address | | `findDatum(txInfo, hash)` | Look up datum by hash | | `valueSpent(txInfo)` | Total value of all inputs | | `valuePaid(txInfo, addr)` | Values paid to an address | | `ownHash(ctx)` | Get own script hash | | `ownInputScriptHash(ctx)` | Get script hash of own input → `byte[]` | | `scriptOutputsAt(txInfo, hash)` | Get outputs at a script hash | | `listIndex(list, index)` | Get element at index | | `trace(msg)` | Emit a trace message | ##### ListsLib | Method | Description | |--------|-------------| | `empty()` | Create an empty list | | `prepend(list, elem)` | Prepend element to list | | `length(list)` | Number of elements | | `isEmpty(list)` | Check if list is empty | | `head(list)` | First element | | `tail(list)` | All elements except first | | `reverse(list)` | Reverse a list | | `concat(a, b)` | Concatenate two lists | | `nth(list, n)` | Element at index n | | `take(list, n)` | First n elements | | `drop(list, n)` | Drop first n elements | | `contains(list, elem)` | Check if list contains element (EqualsData) | | `containsInt(list, target)` | Check if integer list contains value (EqualsInteger) | | `containsBytes(list, target)` | Check if bytestring list contains value (EqualsByteString) | | `hasDuplicateInts(list)` | Check for duplicate integers | | `hasDuplicateBytes(list)` | Check for duplicate bytestrings | | `any(list, pred)` | True if any element matches predicate (HOF) | | `all(list, pred)` | True if all elements match predicate (HOF) | | `find(list, pred)` | Find first matching element (HOF) | | `foldl(f, init, list)` | Left fold (HOF) | | `map(list, f)` | Transform each element (HOF) | | `filter(list, pred)` | Keep elements matching predicate (HOF) | | `zip(a, b)` | Zip two lists into pairs (HOF) | ##### ValuesLib | Method | Description | |--------|-------------| | `lovelaceOf(value)` | Extract lovelace amount | | `assetOf(value, policy, token)` | Extract specific asset amount | | `containsPolicy(value, policy)` | Check if policy exists in value | | `geq(a, b)` | Lovelace-only >= comparison | | `geqMultiAsset(a, b)` | Multi-asset >= comparison | | `leq(a, b)` | Multi-asset <= comparison | | `eq(a, b)` | Multi-asset equality | | `isZero(value)` | Check if all amounts are zero | | `singleton(policy, token, amount)` | Create single-asset Value | | `negate(value)` | Negate all amounts | | `flatten(value)` | Flatten to list of (policy, token, amount) triples | | `flattenTyped(value)` | Flatten to typed `JulcList` with `.policyId()`, `.tokenName()`, `.amount()` | | `add(a, b)` | Add two Values | | `subtract(a, b)` | Subtract value b from value a | | `countTokensWithQty(mint, policy, qty)` | Count tokens with exact quantity under policy | | `findTokenName(mint, policy, qty)` | Find token name with exact quantity under policy | ##### MapLib | Method | Description | |--------|-------------| | `lookup(map, key)` | Look up key; returns Optional (Constr 0/1) | | `member(map, key)` | Check if key exists | | `insert(map, key, value)` | Insert key-value pair | | `delete(map, key)` | Remove key from map | | `keys(map)` | Extract all keys as list | | `values(map)` | Extract all values as list | | `toList(map)` | Convert map to pair list | | `fromList(list)` | Create map from pair list | | `size(map)` | Number of entries | ##### OutputLib | Method | Description | |--------|-------------| | `txOutAddress(txOut)` | Extract address from output | | `txOutValue(txOut)` | Extract value from output | | `txOutDatum(txOut)` | Extract datum from output | | `outputsAt(outputs, address)` | Filter outputs by address | | `countOutputsAt(outputs, address)` | Count outputs at address | | `uniqueOutputAt(outputs, address)` | Exactly one output at address (aborts otherwise) | | `outputsWithToken(outputs, policy, token)` | Filter outputs by token | | `valueHasToken(value, policy, token)` | Check if value contains token | | `lovelacePaidTo(outputs, address)` | Sum lovelace paid to address | | `paidAtLeast(outputs, address, min)` | Check minimum lovelace at address | | `getInlineDatum(txOut)` | Get inline datum (aborts if not inline) | | `resolveDatum(txOut, datumsMap)` | Resolve datum from inline or hash lookup | | `findOutputWithToken(outputs, scriptHash, policy, token)` | Find output at script address with specific token | | `findInputWithToken(inputs, scriptHash, policy, token)` | Find input at script address with specific token | ##### MathLib | Method | Description | |--------|-------------| | `abs(x)` | Absolute value | | `max(a, b)` | Maximum of two integers | | `min(a, b)` | Minimum of two integers | | `pow(base, exp)` | Exponentiation | | `sign(x)` | Sign: -1, 0, or 1 | | `divMod(a, b)` | Division and modulo as Tuple2 | | `quotRem(a, b)` | Quotient and remainder as Tuple2 | | `expMod(base, exp, mod)` | Modular exponentiation | ##### IntervalLib | Method | Description | |--------|-------------| | `contains(interval, point)` | Check if point is within interval | | `always()` | The (-inf, +inf) interval | | `after(t)` | The [t, +inf) interval | | `before(t)` | The (-inf, t] interval | | `between(low, high)` | The [low, high] interval | | `never()` | The empty interval | | `isEmpty(interval)` | Check if interval is empty | | `finiteUpperBound(interval)` | Extract finite upper bound (-1 if infinite) | | `finiteLowerBound(interval)` | Extract finite lower bound (-1 if infinite) | ##### CryptoLib | Method | Description | |--------|-------------| | `sha2_256(bs)` | SHA2-256 hash | | `sha3_256(bs)` | SHA3-256 hash | | `blake2b_256(bs)` | Blake2b-256 hash | | `blake2b_224(bs)` | Blake2b-224 hash (key hashes) | | `keccak_256(bs)` | Keccak-256 hash | | `verifyEd25519Signature(key, msg, sig)` | Verify Ed25519 signature | | `verifyEcdsaSecp256k1(key, msg, sig)` | Verify ECDSA secp256k1 signature | | `verifySchnorrSecp256k1(key, msg, sig)` | Verify Schnorr secp256k1 signature | | `ripemd_160(bs)` | RIPEMD-160 hash | ##### ByteStringLib | Method | Description | |--------|-------------| | `at(bs, index)` | Get byte at index | | `cons(byte_, bs)` | Prepend a byte | | `slice(bs, start, length)` | Extract a slice | | `length(bs)` | Length of bytestring | | `drop(bs, n)` | Drop first n bytes | | `take(bs, n)` | Take first n bytes | | `append(a, b)` | Concatenate two bytestrings | | `empty()` | Empty bytestring | | `zeros(n)` | Bytestring of n zero bytes | | `equals(a, b)` | Equality check | | `lessThan(a, b)` | Lexicographic a < b | | `lessThanEquals(a, b)` | Lexicographic a <= b | | `integerToByteString(endian, width, i)` | Convert integer to bytestring | | `byteStringToInteger(endian, bs)` | Convert bytestring to integer | | `encodeUtf8(s)` | Encode string as UTF-8 bytes | | `decodeUtf8(bs)` | Decode UTF-8 bytes to string | | `serialiseData(d)` | Serialize Data to CBOR bytes | | `hexNibble(n)` | Convert nibble (0-15) to hex ASCII code | | `toHex(bs)` | Convert bytestring to hex-encoded bytestring | | `intToDecimalString(n)` | Convert integer to decimal digit bytestring | | `utf8ToInteger(bs)` | Parse UTF-8 decimal string to integer (inverse of `intToDecimalString`) | ##### BitwiseLib | Method | Description | |--------|-------------| | `andByteString(padding, a, b)` | Bitwise AND | | `orByteString(padding, a, b)` | Bitwise OR | | `xorByteString(padding, a, b)` | Bitwise XOR | | `complementByteString(bs)` | Bitwise complement | | `readBit(bs, index)` | Read bit at index | | `writeBits(bs, indices, value)` | Write bits at indices | | `shiftByteString(bs, n)` | Shift by n bits | | `rotateByteString(bs, n)` | Rotate by n bits | | `countSetBits(bs)` | Count set bits (popcount) | | `findFirstSetBit(bs)` | Index of first set bit (-1 if none) | ##### AddressLib | Method | Description | |--------|-------------| | `credentialHash(address)` | Extract payment credential hash bytes | | `isScriptAddress(address)` | Check if address has ScriptCredential | | `isPubKeyAddress(address)` | Check if address has PubKeyCredential | | `paymentCredential(address)` | Extract payment Credential | --- #### ContextsLib -- Script Context Operations **Import**: `com.bloxbean.cardano.julc.stdlib.lib.ContextsLib` ContextsLib provides access to the Plutus V3 `ScriptContext`, `TxInfo`, and `ScriptInfo` types. For modern validators using typed `ScriptContext`, you can access fields directly (e.g., `ctx.txInfo()`) instead of using the legacy accessor methods. ##### Signatory Check The most common operation: verify that a specific public key hash signed the transaction. ```java import com.bloxbean.cardano.julc.ledger.*; import com.bloxbean.cardano.julc.stdlib.lib.ContextsLib; import java.math.BigInteger; @SpendingValidator class AuthorizedSpend { record Datum(byte[] owner) {} @Entrypoint static boolean validate(Datum datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); return ContextsLib.signedBy(txInfo, datum.owner()); } } ``` ##### TxInfo Field Access You can use either the typed field access on `TxInfo` directly or the ContextsLib accessor methods: ```java @SpendingValidator class FieldAccessExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); // Direct typed field access (preferred) JulcList inputs = txInfo.inputs(); JulcList outputs = txInfo.outputs(); JulcList signatories = txInfo.signatories(); Interval validRange = txInfo.validRange(); Value mint = txInfo.mint(); BigInteger fee = txInfo.fee(); TxId txId = txInfo.id(); // Or via ContextsLib (equivalent, legacy style) JulcList inputs2 = ContextsLib.txInfoInputs(txInfo); Value mint2 = ContextsLib.txInfoMint(txInfo); return true; } } ``` ##### Finding Own Input and Continuing Outputs For spending validators that need to identify their own UTxO and find outputs returning to the same script address: ```java @SpendingValidator class StatefulValidator { record State(BigInteger counter) {} @Entrypoint static boolean validate(State datum, PlutusData redeemer, ScriptContext ctx) { // findOwnInput returns Optional encoded as Constr(0, [txInInfo]) or Constr(1, []) PlutusData.ConstrData ownInputOpt = ContextsLib.findOwnInput(ctx); // getContinuingOutputs finds outputs to the same script address PlutusData.ListData continuingOutputs = ContextsLib.getContinuingOutputs(ctx); // Get own script hash (works for both minting and spending) PlutusData.BytesData ownHash = ContextsLib.ownHash(ctx); // Get script hash of own input as byte[] byte[] scriptHash = ContextsLib.ownInputScriptHash(ctx); return !Builtins.nullList(continuingOutputs); } } ``` ##### Trace Messages Emit debug trace messages that appear in transaction evaluation logs: ```java @SpendingValidator class TracingValidator { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { ContextsLib.trace("Starting validation"); TxInfo txInfo = ctx.txInfo(); if (ListsLib.isEmpty(txInfo.inputs())) { ContextsLib.trace("No inputs found"); return false; } ContextsLib.trace("Validation passed"); return true; } } ``` --- #### ListsLib -- List Operations **Import**: `com.bloxbean.cardano.julc.stdlib.lib.ListsLib` ListsLib provides list construction, traversal, searching, and higher-order functions. In Plutus, lists are singly-linked (cons lists). Most operations are O(n). ##### Basic List Operations ```java @SpendingValidator class ListExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); JulcList outputs = txInfo.outputs(); // Size and emptiness long count = outputs.size(); // or ListsLib.length(outputs) boolean empty = outputs.isEmpty(); // or ListsLib.isEmpty(outputs) // Element access TxOut first = outputs.head(); // or ListsLib.head(outputs) JulcList rest = outputs.tail(); // or ListsLib.tail(outputs) return count > 0; } } ``` ##### Search Operations ```java @SpendingValidator class ListSearchExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); JulcList signatories = txInfo.signatories(); // Check for duplicates (O(n^2)) boolean hasDups = ListsLib.hasDuplicateBytes(signatories); return !hasDups; } } ``` ##### Higher-Order Functions (Lambda Required) The HOF methods (`any`, `all`, `find`, `foldl`, `map`, `filter`, `zip`) accept lambda expressions. These are compiled via PIR and require lambda support. ```java @SpendingValidator class HofExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); JulcList outputs = txInfo.outputs(); // any: true if any output has more than 5 ADA boolean hasLargeOutput = ListsLib.any(outputs, out -> ValuesLib.lovelaceOf(out.value()) > 5_000_000); // all: true if all outputs go to pub key addresses boolean allPubKey = ListsLib.all(outputs, out -> AddressLib.isPubKeyAddress(out.address())); // filter: keep only outputs above 2 ADA JulcList largeOutputs = ListsLib.filter(outputs, out -> ValuesLib.lovelaceOf(out.value()) > 2_000_000); // foldl: sum all output lovelace BigInteger totalLovelace = ListsLib.foldl( (acc, out) -> acc + ValuesLib.lovelaceOf(out.value()), BigInteger.ZERO, outputs); return hasLargeOutput; } } ``` These HOF methods are also available as instance methods on `JulcList`. Lambda parameter types are auto-inferred from the list element type: ```java // Instance method equivalents boolean hasLargeOutput = outputs.any( out -> ValuesLib.lovelaceOf(out.value()) > 5_000_000); JulcList largeOutputs = outputs.filter( out -> ValuesLib.lovelaceOf(out.value()) > 2_000_000); // Chaining is supported var result = outputs.filter(out -> isLarge(out)).map(out -> transform(out)); ``` `foldl` is only available as a static call (`ListsLib.foldl`) because it takes two lambda parameters plus an initial value. ##### For-Each Loops JuLC supports for-each iteration over lists directly, which is often more readable than HOFs: ```java @SpendingValidator class ForEachExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); BigInteger total = BigInteger.ZERO; for (TxOut out : txInfo.outputs()) { total = total.add(ValuesLib.lovelaceOf(out.value())); } return total.compareTo(BigInteger.valueOf(10_000_000)) > 0; } } ``` --- #### ValuesLib -- Value Manipulation **Import**: `com.bloxbean.cardano.julc.stdlib.lib.ValuesLib` ValuesLib operates on Plutus `Value` types, which are nested maps: `Map>`. Lovelace is stored under the empty bytestring policy and token name. ##### Extracting Amounts ```java @SpendingValidator class ValueExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); TxOut firstOutput = txInfo.outputs().head(); Value outputValue = firstOutput.value(); // Extract lovelace BigInteger lovelace = ValuesLib.lovelaceOf(outputValue); // Extract a specific native token amount byte[] policyId = new byte[]{/* ... */}; byte[] tokenName = new byte[]{/* ... */}; BigInteger tokenAmount = ValuesLib.assetOf(outputValue, policyId, tokenName); // Check if a policy exists boolean hasPolicy = ValuesLib.containsPolicy(outputValue, policyId); return lovelace.compareTo(BigInteger.valueOf(2_000_000)) >= 0; } } ``` ##### Value Comparisons ```java @SpendingValidator class ValueComparisonExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); TxOut out1 = txInfo.outputs().head(); TxOut out2 = txInfo.outputs().tail().head(); Value v1 = out1.value(); Value v2 = out2.value(); // Multi-asset comparison (checks ALL policy/token pairs) boolean v1GreaterOrEqual = ValuesLib.geqMultiAsset(v1, v2); boolean v1LessOrEqual = ValuesLib.leq(v1, v2); boolean valuesEqual = ValuesLib.eq(v1, v2); boolean valueIsZero = ValuesLib.isZero(v1); // Lovelace-only comparison boolean lovelaceGeq = ValuesLib.geq(v1, v2); return v1GreaterOrEqual; } } ``` ##### Value Arithmetic ```java @SpendingValidator class ValueArithmeticExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); Value inputValue = txInfo.inputs().head().resolved().value(); Value outputValue = txInfo.outputs().head().value(); // Create a singleton value byte[] policy = new byte[]{/* ... */}; byte[] token = new byte[]{/* ... */}; Value fee = ValuesLib.singleton(policy, token, BigInteger.valueOf(100)); // Add and subtract values Value combined = ValuesLib.add(inputValue, fee); Value difference = ValuesLib.subtract(inputValue, outputValue); // Negate a value (flip all amounts) Value negated = ValuesLib.negate(fee); // Flatten to inspect all assets PlutusData.ListData triples = ValuesLib.flatten(inputValue); return ValuesLib.geqMultiAsset(inputValue, outputValue); } } ``` ##### Typed Asset Iteration `ValuesLib.flattenTyped()` returns a `JulcList` for type-safe iteration over all assets in a Value. Each `AssetEntry` provides `.policyId()`, `.tokenName()`, and `.amount()` field access without manual destructuring. ```java @SpendingValidator class TokenLeakCheck { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { TxOut output = ctx.txInfo().outputs().head(); long nonAdaCount = 0; for (AssetEntry asset : ValuesLib.flattenTyped(output.value())) { byte[] policy = asset.policyId(); byte[] name = asset.tokenName(); BigInteger amount = asset.amount(); if (Builtins.lengthOfByteString(policy) > 0) { nonAdaCount = nonAdaCount + 1; } } return nonAdaCount == 1; } } ``` > **Tip:** Use `flattenTyped()` instead of `flatten()` whenever you need to inspect individual assets. The raw `flatten()` returns `PlutusData.ListData` requiring manual `Builtins.constrFields()` + `headList`/`tailList` destructuring. --- #### MapLib -- Association List Operations **Import**: `com.bloxbean.cardano.julc.stdlib.lib.MapLib` In Plutus, maps are association lists (`List>`), not hash maps. Lookups are O(n). Insert prepends (shadowing existing keys). ##### Basic Map Operations ```java @SpendingValidator class MapExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); // Withdrawals is a Map JulcMap withdrawals = txInfo.withdrawals(); // Check membership and lookup PlutusData key = /* some key */; boolean exists = withdrawals.containsKey(key); // Size long mapSize = withdrawals.size(); return exists; } } ``` ##### Map Modification ```java @SpendingValidator class MapModifyExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { PlutusData.MapData myMap = Builtins.mapData(Builtins.mkNilPairData()); // Insert entries PlutusData key1 = Builtins.iData(BigInteger.ONE); PlutusData val1 = Builtins.iData(BigInteger.valueOf(100)); myMap = MapLib.insert(myMap, key1, val1); PlutusData key2 = Builtins.iData(BigInteger.valueOf(2)); PlutusData val2 = Builtins.iData(BigInteger.valueOf(200)); myMap = MapLib.insert(myMap, key2, val2); // Lookup returns Optional: Constr(0, [value]) or Constr(1, []) PlutusData.ConstrData result = MapLib.lookup(myMap, key1); boolean found = Builtins.constrTag(result) == 0; // Delete a key myMap = MapLib.delete(myMap, key1); // Extract keys and values as lists PlutusData.ListData allKeys = MapLib.keys(myMap); PlutusData.ListData allValues = MapLib.values(myMap); return MapLib.size(myMap) == 1; } } ``` ##### Iterating Over Maps Use for-each with `JulcMap` or iterate over the pair list: ```java @SpendingValidator class MapIterateExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); JulcMap withdrawals = txInfo.withdrawals(); // For-each on a map iterates over key-value pairs BigInteger totalWithdrawn = BigInteger.ZERO; for (var entry : withdrawals) { BigInteger amount = entry.value(); totalWithdrawn = totalWithdrawn.add(amount); } return totalWithdrawn.compareTo(BigInteger.ZERO) > 0; } } ``` --- #### OutputLib -- Transaction Output Utilities **Import**: `com.bloxbean.cardano.julc.stdlib.lib.OutputLib` OutputLib provides high-level operations for filtering and inspecting transaction outputs. It uses typed ledger types (`TxOut`, `Address`, `Value`, `OutputDatum`). ##### Filtering Outputs ```java @SpendingValidator class OutputFilterExample { record Datum(Address recipient) {} @Entrypoint static boolean validate(Datum datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); JulcList outputs = txInfo.outputs(); // Filter outputs by address JulcList recipientOutputs = OutputLib.outputsAt(outputs, datum.recipient()); // Count outputs at address long count = OutputLib.countOutputsAt(outputs, datum.recipient()); // Get the unique output at an address (aborts if != 1) TxOut uniqueOutput = OutputLib.uniqueOutputAt(outputs, datum.recipient()); return count >= 1; } } ``` ##### Token Filtering ```java @SpendingValidator class TokenFilterExample { record Datum(byte[] policyId, byte[] tokenName) {} @Entrypoint static boolean validate(Datum datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); JulcList outputs = txInfo.outputs(); // Find all outputs containing a specific token JulcList tokenOutputs = OutputLib.outputsWithToken( outputs, datum.policyId(), datum.tokenName()); // Check if a specific output has a token TxOut firstOutput = outputs.head(); boolean hasToken = OutputLib.valueHasToken( firstOutput.value(), datum.policyId(), datum.tokenName()); return !tokenOutputs.isEmpty(); } } ``` ##### Lovelace Payment Checks ```java @SpendingValidator class PaymentCheckExample { record Datum(Address recipient, BigInteger minPayment) {} @Entrypoint static boolean validate(Datum datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); JulcList outputs = txInfo.outputs(); // Sum lovelace paid to an address BigInteger totalPaid = OutputLib.lovelacePaidTo(outputs, datum.recipient()); // Check if minimum payment is met boolean sufficient = OutputLib.paidAtLeast( outputs, datum.recipient(), datum.minPayment()); return sufficient; } } ``` ##### Datum Extraction ```java @SpendingValidator class DatumExample { record MyDatum(BigInteger value) {} @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); TxOut output = txInfo.outputs().head(); // Get inline datum directly (aborts if not inline) PlutusData inlineDatum = OutputLib.getInlineDatum(output); // Or resolve datum (handles both inline and hash-based) // Pass the datums map from TxInfo PlutusData.MapData datumsMap = (PlutusData.MapData)(Object) txInfo.datums(); PlutusData resolvedDatum = OutputLib.resolveDatum(output, datumsMap); return true; } } ``` --- #### MathLib -- Mathematical Operations **Import**: `com.bloxbean.cardano.julc.stdlib.lib.MathLib` MathLib provides common mathematical functions operating on `BigInteger`. All computations use Plutus integer arithmetic. ##### Basic Math ```java @SpendingValidator class MathExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { BigInteger a = BigInteger.valueOf(42); BigInteger b = BigInteger.valueOf(-10); BigInteger absVal = MathLib.abs(b); // 10 BigInteger maxVal = MathLib.max(a, b); // 42 BigInteger minVal = MathLib.min(a, b); // -10 BigInteger signVal = MathLib.sign(b); // -1 BigInteger powVal = MathLib.pow(a, BigInteger.valueOf(3)); // 42^3 return absVal.compareTo(BigInteger.ZERO) > 0; } } ``` ##### Division and Modular Arithmetic `divMod` and `quotRem` return `Tuple2`. Use `.first()` and `.second()` to access results. ```java import com.bloxbean.cardano.julc.core.types.Tuple2; @SpendingValidator class DivModExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { BigInteger a = BigInteger.valueOf(17); BigInteger b = BigInteger.valueOf(5); // divMod returns (quotient, remainder) Tuple2 dm = MathLib.divMod(a, b); BigInteger quotient = dm.first(); // 3 BigInteger remainder = dm.second(); // 2 // Modular exponentiation: base^exp mod modulus BigInteger result = MathLib.expMod( BigInteger.valueOf(2), BigInteger.valueOf(10), BigInteger.valueOf(1000)); // 1024 mod 1000 = 24 return remainder.compareTo(BigInteger.ZERO) >= 0; } } ``` --- #### IntervalLib -- Time Interval Operations **Import**: `com.bloxbean.cardano.julc.stdlib.lib.IntervalLib` IntervalLib operates on Plutus `Interval` (POSIXTimeRange) types. Use these for time-locked validators. Time values are POSIX milliseconds as `BigInteger`. ##### Time-Locked Validator ```java import com.bloxbean.cardano.julc.ledger.*; import com.bloxbean.cardano.julc.stdlib.lib.IntervalLib; import com.bloxbean.cardano.julc.stdlib.lib.ContextsLib; @SpendingValidator class TimeLockValidator { record Datum(byte[] beneficiary, BigInteger deadline) {} @Entrypoint static boolean validate(Datum datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); Interval validRange = txInfo.validRange(); // Check if the transaction's valid range falls after the deadline BigInteger lowerBound = IntervalLib.finiteLowerBound(validRange); boolean pastDeadline = lowerBound.compareTo(datum.deadline()) >= 0; // Check if beneficiary signed boolean signed = ContextsLib.signedBy(txInfo, datum.beneficiary()); return pastDeadline && signed; } } ``` ##### Interval Construction and Checks ```java @SpendingValidator class IntervalExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); Interval txRange = txInfo.validRange(); // Check if a specific point is within the transaction's valid range BigInteger checkTime = BigInteger.valueOf(1700000000000L); boolean timeInRange = IntervalLib.contains(txRange, checkTime); // Construct intervals Interval alwaysValid = IntervalLib.always(); Interval neverValid = IntervalLib.never(); Interval afterNoon = IntervalLib.after(BigInteger.valueOf(1700000000000L)); Interval beforeMidnight = IntervalLib.before(BigInteger.valueOf(1700100000000L)); Interval window = IntervalLib.between( BigInteger.valueOf(1700000000000L), BigInteger.valueOf(1700100000000L)); // Check emptiness boolean empty = IntervalLib.isEmpty(neverValid); // Extract bounds BigInteger upper = IntervalLib.finiteUpperBound(txRange); BigInteger lower = IntervalLib.finiteLowerBound(txRange); return timeInRange; } } ``` --- #### CryptoLib -- Cryptographic Operations **Import**: `com.bloxbean.cardano.julc.stdlib.lib.CryptoLib` CryptoLib wraps Plutus cryptographic builtins for hashing and signature verification. These are also available directly via `Builtins`. ##### Hash Functions ```java @SpendingValidator class HashExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { byte[] data = new byte[]{1, 2, 3}; byte[] sha256 = CryptoLib.sha2_256(data); byte[] sha3 = CryptoLib.sha3_256(data); byte[] blake256 = CryptoLib.blake2b_256(data); byte[] blake224 = CryptoLib.blake2b_224(data); // Used for key hashes byte[] keccak = CryptoLib.keccak_256(data); byte[] ripemd = CryptoLib.ripemd_160(data); return Builtins.lengthOfByteString(sha256) == 32; } } ``` ##### Signature Verification ```java @SpendingValidator class SigVerifyExample { record Datum(byte[] pubKey, byte[] message) {} @Entrypoint static boolean validate(Datum datum, byte[] signature, ScriptContext ctx) { // Ed25519 signature verification boolean valid = CryptoLib.verifyEd25519Signature( datum.pubKey(), datum.message(), signature); return valid; } } ``` ##### ECDSA and Schnorr (secp256k1) ```java @SpendingValidator class Secp256k1Example { record Datum(byte[] key, byte[] msgHash) {} @Entrypoint static boolean validate(Datum datum, byte[] sig, ScriptContext ctx) { // ECDSA secp256k1 (Ethereum-compatible) boolean ecdsaValid = CryptoLib.verifyEcdsaSecp256k1( datum.key(), datum.msgHash(), sig); // Schnorr secp256k1 (Bitcoin Taproot compatible) boolean schnorrValid = CryptoLib.verifySchnorrSecp256k1( datum.key(), datum.msgHash(), sig); return ecdsaValid || schnorrValid; } } ``` --- #### ByteStringLib -- ByteString Operations **Import**: `com.bloxbean.cardano.julc.stdlib.lib.ByteStringLib` ByteStringLib provides operations on `byte[]` (ByteString in Plutus). Includes slicing, comparison, and encoding/serialization. ##### Slicing and Manipulation ```java @SpendingValidator class ByteStringExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { byte[] data = new byte[]{0x01, 0x02, 0x03, 0x04, 0x05}; // Length long len = ByteStringLib.length(data); // 5 // Element access long firstByte = ByteStringLib.at(data, 0); // 1 // Slicing byte[] firstTwo = ByteStringLib.take(data, 2); // [0x01, 0x02] byte[] lastThree = ByteStringLib.drop(data, 2); // [0x03, 0x04, 0x05] byte[] middle = ByteStringLib.slice(data, 1, 3); // [0x02, 0x03, 0x04] // Construction byte[] withPrefix = ByteStringLib.cons(0xFF, data); byte[] combined = ByteStringLib.append(firstTwo, lastThree); byte[] emptyBs = ByteStringLib.empty(); byte[] zeroes = ByteStringLib.zeros(32); return len == 5; } } ``` ##### Comparison and Conversion ```java @SpendingValidator class ByteStringCompareExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { byte[] a = new byte[]{0x01, 0x02}; byte[] b = new byte[]{0x01, 0x03}; // Comparison boolean eq = ByteStringLib.equals(a, b); boolean lt = ByteStringLib.lessThan(a, b); boolean lte = ByteStringLib.lessThanEquals(a, b); // Integer <-> ByteString conversion byte[] encoded = ByteStringLib.integerToByteString(true, 8, 256); long decoded = ByteStringLib.byteStringToInteger(true, encoded); // Data serialization (to CBOR) PlutusData someData = Builtins.iData(BigInteger.valueOf(42)); byte[] cbor = ByteStringLib.serialiseData(someData); return lt; } } ``` ##### UTF-8 Decimal Parsing `ByteStringLib.utf8ToInteger()` parses a UTF-8-encoded decimal string into an integer. This is the inverse of `intToDecimalString()`. ```java // Parse "42" bytes → integer 42 byte[] bs = "42".getBytes(); BigInteger n = ByteStringLib.utf8ToInteger(bs); // 42 // Roundtrip property: // ByteStringLib.utf8ToInteger(ByteStringLib.intToDecimalString(n)) == n ``` --- #### BitwiseLib -- Bitwise Operations **Import**: `com.bloxbean.cardano.julc.stdlib.lib.BitwiseLib` BitwiseLib provides bit-level operations on `byte[]`. The `padding` parameter in AND/OR/XOR controls behavior when bytestrings have different lengths (`true` = zero-extend shorter, `false` = truncate longer). ##### Bitwise Logical Operations ```java @SpendingValidator class BitwiseExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { byte[] a = new byte[]{(byte) 0xFF, (byte) 0x0F}; byte[] b = new byte[]{(byte) 0x0F, (byte) 0xF0}; // Bitwise operations (padding=false truncates to shorter length) byte[] andResult = BitwiseLib.andByteString(false, a, b); // [0x0F, 0x00] byte[] orResult = BitwiseLib.orByteString(false, a, b); // [0xFF, 0xFF] byte[] xorResult = BitwiseLib.xorByteString(false, a, b); // [0xF0, 0xFF] byte[] complement = BitwiseLib.complementByteString(a); // [0x00, 0xF0] return true; } } ``` ##### Bit Manipulation and Counting ```java @SpendingValidator class BitManipExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { byte[] data = new byte[]{(byte) 0b10110100}; // Read individual bits boolean bit0 = BitwiseLib.readBit(data, 0); // Count set bits (popcount) long popcount = BitwiseLib.countSetBits(data); // 4 // Find first set bit long firstSet = BitwiseLib.findFirstSetBit(data); // Shift and rotate byte[] shifted = BitwiseLib.shiftByteString(data, 2); byte[] rotated = BitwiseLib.rotateByteString(data, 2); return popcount > 0; } } ``` --- #### AddressLib -- Address Operations **Import**: `com.bloxbean.cardano.julc.stdlib.lib.AddressLib` AddressLib inspects Plutus `Address` types: extracting credential hashes and checking whether an address is a script or public key address. ##### Address Inspection ```java @SpendingValidator class AddressExample { @Entrypoint static boolean validate(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); TxOut output = txInfo.outputs().head(); Address addr = output.address(); // Extract the payment credential hash (works for both PubKey and Script) byte[] credHash = AddressLib.credentialHash(addr); // Check address type boolean isScript = AddressLib.isScriptAddress(addr); boolean isPubKey = AddressLib.isPubKeyAddress(addr); // Extract the full credential (for pattern matching) Credential cred = AddressLib.paymentCredential(addr); return isPubKey; } } ``` ##### Enforcing Output Destination ```java @SpendingValidator class DestinationCheckExample { record Datum(byte[] allowedRecipient) {} @Entrypoint static boolean validate(Datum datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); boolean allOutputsValid = true; for (TxOut out : txInfo.outputs()) { Address addr = out.address(); if (AddressLib.isPubKeyAddress(addr)) { byte[] hash = AddressLib.credentialHash(addr); if (!Builtins.equalsByteString(hash, datum.allowedRecipient())) { allOutputsValid = false; } else { allOutputsValid = allOutputsValid; } } else { allOutputsValid = allOutputsValid; } } return allOutputsValid; } } ``` --- #### BlsLib -- BLS12-381 Curve Operations `BlsLib` wraps the Plutus V3 BLS12-381 builtins for elliptic curve cryptography. All methods are `static` and available on PV10+. ##### Quick Reference | Method | Description | |--------|-------------| | `g1Add(a, b)` | Add two G1 elements | | `g1Neg(a)` | Negate a G1 element | | `g1ScalarMul(scalar, g1)` | Scalar multiplication of G1 | | `g1Equal(a, b)` | Check G1 equality | | `g1Compress(g1)` | Compress G1 to bytes | | `g1Uncompress(compressed)` | Uncompress bytes to G1 | | `g1HashToGroup(msg, dst)` | Hash to G1 | | `g2Add(a, b)` | Add two G2 elements | | `g2Neg(a)` | Negate a G2 element | | `g2ScalarMul(scalar, g2)` | Scalar multiplication of G2 | | `g2Equal(a, b)` | Check G2 equality | | `g2Compress(g2)` | Compress G2 to bytes | | `g2Uncompress(compressed)` | Uncompress bytes to G2 | | `g2HashToGroup(msg, dst)` | Hash to G2 | | `millerLoop(g1, g2)` | Compute Miller loop pairing | | `mulMlResult(a, b)` | Multiply two Miller loop results | | `finalVerify(a, b)` | Final pairing verification | | `g1MultiScalarMul(scalars, points)` | Multi-scalar multiplication on G1 | | `g2MultiScalarMul(scalars, points)` | Multi-scalar multiplication on G2 | ##### Usage ```java import com.bloxbean.cardano.julc.stdlib.lib.BlsLib; // G1 operations var sum = BlsLib.g1Add(pointA, pointB); var scaled = BlsLib.g1ScalarMul(scalar, point); boolean eq = BlsLib.g1Equal(a, b); // Pairing var ml = BlsLib.millerLoop(g1Point, g2Point); boolean valid = BlsLib.finalVerify(ml1, ml2); ``` > **Off-chain:** BlsLib methods throw `UnsupportedOperationException` — use `JulcEval.forSource()` for UPLC evaluation. --- #### NativeValueLib -- Native Value Operations (PV11) > **Protocol Version 11+ only.** These operations use native UPLC Value builtins (CIP-153) available from PV11 onwards. They will not work on PV10 networks. For PV10 networks, use `ValuesLib` which operates on Map-encoded PlutusData. `NativeValueLib` provides efficient native MaryEra Value operations via PV11 builtins. ##### Quick Reference | Method | Description | |--------|-------------| | `insertCoin(policyId, tokenName, amount, value)` | Insert/update token quantity | | `lookupCoin(policyId, tokenName, value)` | Look up token quantity (0 if absent) | | `union(a, b)` | Merge two Values by adding quantities | | `contains(a, b)` | Check a >= b element-wise | | `scale(scalar, value)` | Scale all quantities | | `fromData(mapData)` | Convert Map-encoded PlutusData to native Value | | `toData(value)` | Convert native Value back to Map encoding | ##### Usage ```java import com.bloxbean.cardano.julc.stdlib.lib.NativeValueLib; // Build a Value with tokens var value = NativeValueLib.insertCoin(policyId, tokenName, BigInteger.valueOf(100), emptyValue); // Check if one value contains another boolean ok = NativeValueLib.contains(outputValue, requiredValue); // Merge values var total = NativeValueLib.union(valueA, valueB); ``` > **Off-chain:** NativeValueLib methods throw `UnsupportedOperationException` — use `JulcEval.forSource()` for UPLC evaluation. --- #### Important Notes and Caveats ##### On-Chain vs Off-Chain All stdlib libraries are annotated with `@OnchainLibrary` and compile to UPLC for on-chain execution. Some methods use casts like `(PlutusData)(Object)` that are no-ops on-chain but may throw `ClassCastException` off-chain. Methods that work off-chain are noted in the source Javadoc. For off-chain testing, use UPLC evaluation via the JuLC testkit rather than calling library methods directly in JVM code. ##### Loop Patterns JuLC requires immutable variable semantics. In for-each and while loops used as accumulators, both branches of an `if` must assign the accumulator variable: ```java // Correct: both branches assign result for (TxOut out : outputs) { if (someCondition) { result = result.prepend(out); } else { result = result; // identity assignment required } } ``` ##### Cross-Library BytesData Parameter Bug When calling stdlib methods that take `BytesData`/`MapData` typed parameters from user code, pass `PlutusData` (not the specific subtype) to avoid type confusion at the UPLC boundary. See the project MEMORY.md for details. ##### Value.assetOf() Needs BData Arguments When constructing policy IDs or token names to pass to `ValuesLib.assetOf()`, use the `byte[]` overload rather than manually wrapping with `Builtins.bData()`. The library handles wrapping internally: ```java // Correct: pass byte[] directly BigInteger amount = ValuesLib.assetOf(value, policyId, tokenName); // The library internally wraps with bData for the UPLC comparison ``` ##### ownHash + containsPolicy Pattern When checking if a minting policy's own token exists in a value (common in minting validators), use the `(byte[])(Object)` cast on `ContextsLib.ownHash()`: ```java @MintingValidator class MyTokenPolicy { @Entrypoint static boolean validate(PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); byte[] ownPolicy = (byte[])(Object) ContextsLib.ownHash(ctx); // Check if minted value contains our policy boolean hasMint = ValuesLib.containsPolicy(txInfo.mint(), ownPolicy); // Or get a specific token amount BigInteger qty = ValuesLib.assetOf(txInfo.mint(), ownPolicy, "TOKEN".getBytes()); return hasMint && qty.equals(BigInteger.ONE); } } ``` The `(byte[])(Object)` cast is required because `ownHash()` returns a `ValidatorHash` (which is `ByteStringType` at UPLC level but a different Java type at source level). The cast is a no-op at UPLC level. ##### MapLib.lookup Returns Optional Encoding `MapLib.lookup()` returns an Optional encoded as Plutus Data: - `Constr(0, [value])` for Some (found) - `Constr(1, [])` for None (not found) Check the tag with `Builtins.constrTag(result) == 0` to determine if the lookup succeeded: ```java PlutusData.ConstrData result = MapLib.lookup(myMap, key); if (Builtins.constrTag(result) == 0) { PlutusData value = Builtins.headList(Builtins.constrFields(result)); // use value } ``` ##### map() Returns JulcList The `map` HOF wraps each lambda result to Data, so the returned list always has `PlutusData` elements regardless of input type. Use `Builtins.unIData()` or `Builtins.unBData()` to extract typed values from mapped results. ##### Typed Field Access vs Library Methods For `ScriptContext` and `TxInfo`, prefer direct typed field access over the ContextsLib wrapper methods: ```java // Preferred: direct access TxInfo txInfo = ctx.txInfo(); JulcList outputs = txInfo.outputs(); // Legacy (still works) TxInfo txInfo = ContextsLib.getTxInfo(ctx); JulcList outputs = ContextsLib.txInfoOutputs(txInfo); ``` ##### Methods Also Available via Instance Methods Many list and map operations are available as instance methods on `JulcList` and `JulcMap`, which can be more readable: ```java JulcList outputs = txInfo.outputs(); // Library style long count = ListsLib.length(outputs); boolean empty = ListsLib.isEmpty(outputs); TxOut first = ListsLib.head(outputs); // Instance method style (equivalent) long count = outputs.size(); boolean empty = outputs.isEmpty(); TxOut first = outputs.head(); // HOF methods are also available as instance calls boolean anyLarge = outputs.any(out -> isLarge(out)); JulcList filtered = outputs.filter(out -> isLarge(out)); JulcList mapped = outputs.map(out -> transform(out)); ``` ##### PV11 (Protocol Version 11) Features The following features require protocol version 11 or later and will not work on PV10 networks: - **NativeValueLib** — Native MaryEra Value operations (CIP-153) - **JulcArray\** — Immutable arrays with O(1) random access (CIP-156) - **Builtins.dropList()** — Drop first n elements from a list (CIP-158) These are experimental APIs that may evolve as PV11 stabilizes. BlsLib and all other existing stdlib libraries work on PV10+. --- # Reference --- ## JuLC API Reference Source: https://julc.dev/reference/api-reference/ > JuLC API Reference - JuLC documentation This document covers all supported Java operations in the JuLC compiler, the standard library functions, and the typed ledger access API. #### Supported Types | Java Type | UPLC Representation | Notes | |-----------|-------------------|-------| | `int`, `long`, `BigInteger` | Integer | All mapped to arbitrary-precision integers | | `boolean` | Bool (Constr 0/1) | `false` = Constr(0,[]), `true` = Constr(1,[]) | | `byte[]` | ByteString | Raw bytes | | `String` | String (UTF-8) | Converted via EncodeUtf8/DecodeUtf8 | | `PlutusData` | Data | Opaque on-chain data | | `List`, `JulcList` | BuiltinList | Builtin list of Data | | `Map`, `JulcMap` | BuiltinList(Pair) | Map encoded as list of pairs | | `Optional` | Constr 0/1 | Some = Constr(0,[x]), None = Constr(1,[]) | | `Tuple2` | Constr(0, [a, b]) | Generic pair with auto-unwrap | | `Tuple3` | Constr(0, [a, b, c]) | Generic triple with auto-unwrap | | Records | Constr(0, fields) | Each field is a Data element | | Sealed interfaces | Constr(tag, fields) | Tag based on permit order | | `@NewType` records | Underlying type | Zero-cost alias (identity on-chain) | | `PubKeyHash`, `PolicyId`, `TokenName`, `TxId`, `ScriptHash`, `ValidatorHash`, `DatumHash` | ByteString | Ledger hash types | | `Value` | Map(Pair) | Named RecordType with instance methods | | `JulcArray` *(PV11)* | BuiltinArray | O(1) random access array (CIP-156). PV11+ only | ##### JulcList and JulcMap `JulcList` and `JulcMap` are interfaces in `julc-core/types/` that resolve to the same `ListType`/`MapType` as `List`/`Map`. They provide IDE autocomplete for on-chain methods (`.contains()`, `.size()`, `.get()`, etc.). ```java JulcList signers = txInfo.signatories(); // typed list of PubKeyHash JulcMap withdrawals = txInfo.withdrawals(); ``` ##### JulcArray (PV11) > **Protocol Version 11+ only.** Arrays use CIP-156 builtins and are not available on PV10 networks. `JulcArray` is an immutable array interface providing O(1) random access on-chain. Create from a list via `list.toArray()` or `JulcArray.fromList(list)`. ```java JulcList list = ...; JulcArray arr = list.toArray(); // ListToArray builtin BigInteger elem = arr.get(0); // IndexArray builtin (O(1)) long len = arr.length(); // LengthOfArray builtin ``` Off-chain: backed by `JulcArrayImpl`, wrapping `java.util.List` with O(1) index access. ##### Tuple2 and Tuple3 Generic tuples with auto-unwrapping field access based on type arguments. ```java Tuple2 result = MathLib.divMod(a, b); BigInteger quotient = result.first(); // auto-generates UnIData byte[] remainder = result.second(); // auto-generates UnBData // Construction auto-wraps var t = new Tuple2(val1, val2); // auto-wraps via IData ``` Raw `Tuple2` (no type args) defaults to `DataType` for backward compatibility. Tuple2/Tuple3 are **not switchable** (registered as RecordType, but switch requires SumType). Use field access instead. ##### @NewType Zero-cost type aliases for single-field records with a supported underlying type (`byte[]`, `BigInteger`, `String`, `boolean`). ```java @NewType public record AssetClass(byte[] policyId) {} // On-chain: identity (no ConstrData wrap) // AssetClass.of(bytes) is auto-registered ``` ##### Type.of() Factory Methods Seven ledger hash types have `.of(byte[])` factory methods: | Type | Usage | On-chain | |------|-------|----------| | `PubKeyHash.of(bytes)` | Create from raw bytes | Identity | | `ScriptHash.of(bytes)` | | Identity | | `ValidatorHash.of(bytes)` | | Identity | | `PolicyId.of(bytes)` | | Identity | | `TokenName.of(bytes)` | | Identity | | `DatumHash.of(bytes)` | | Identity | | `TxId.of(bytes)` | | Identity | These replace the ugly `(PubKeyHash)(Object) bytes` casts in user code. ##### PlutusData.cast() Helper | Signature | On-chain | Off-chain | |-----------|----------|-----------| | `PlutusData.cast(data, TargetType.class)` | Identity (zero cost) | Unchecked cast | Replaces the `(TargetType)(Object) data` double-cast pattern. Works with records, sealed interfaces, ledger types, `JulcMap`, `byte[]`, and hash types. The second argument must be a literal `ClassName.class` expression. For generic collections (`JulcList`, `JulcMap`), use an explicit type declaration to preserve element types: ```java JulcList items = PlutusData.cast(data, JulcList.class); // element type: MyRecord JulcMap m = PlutusData.cast(data, JulcMap.class); // typed keys + values var items2 = PlutusData.cast(data, JulcList.class); // element type: DataType (avoid) ``` ##### Not Supported `float`, `double`, `char`, arrays (`T[]` — use `JulcArray` on PV11+ or `List`), `null`, collections other than `List`/`Map`/`Optional`/`JulcArray`, generic classes, inheritance (use sealed interfaces instead). #### Operators ##### Arithmetic | Operator | Java Example | UPLC Builtin | Notes | |----------|-------------|-------------|-------| | `+` | `a + b` | `AddInteger` | For `BigInteger`/`int`/`long` operands | | `+` | `s1 + s2` | `AppendString` | For `String` operands | | `+` | `b1 + b2` | `AppendByteString` | For `byte[]` operands | | `-` | `a - b` | `SubtractInteger` | | | `*` | `a * b` | `MultiplyInteger` | | | `/` | `a / b` | `DivideInteger` | | | `%` | `a % b` | `RemainderInteger` | | The `+` operator is type-aware: the compiler infers the operand type and dispatches to the correct builtin. ##### Comparison | Operator | Java Example | UPLC Builtin | Notes | |----------|-------------|-------------|-------| | `==` | `a == b` | `EqualsInteger` | For `BigInteger`/`int`/`long` (default) | | `==` | `s1 == s2` | `EqualsString` | For `String` operands | | `==` | `b1 == b2` | `EqualsByteString` | For `byte[]` operands | | `==` | `d1 == d2` | `EqualsData` | For `PlutusData`, records, sealed interfaces | | `==` | `x == y` | `IfThenElse` | For `boolean` operands | | `!=` | `a != b` | negated equality | Same type dispatch as `==` | | `<` | `a < b` | `LessThanInteger` | | | `<=` | `a <= b` | `LessThanEqualsInteger` | | | `>` | `a > b` | `LessThanInteger` (swapped) | | | `>=` | `a >= b` | `LessThanEqualsInteger` (swapped) | | ##### Boolean | Operator | Java Example | UPLC Translation | |----------|-------------|-----------------| | `&&` | `a && b` | `IfThenElse(a, b, false)` (short-circuit) | | `\|\|` | `a \|\| b` | `IfThenElse(a, true, b)` (short-circuit) | | `!` | `!a` | `IfThenElse(a, false, true)` | #### Control Flow ##### If/Else ```java if (condition) { return x; } else { return y; } ``` ##### Ternary ```java return condition ? x : y; ``` ##### Switch Pattern Matching (Sealed Interfaces) ```java sealed interface Action permits Deposit, Withdraw {} record Deposit(BigInteger amount) implements Action {} record Withdraw(BigInteger amount) implements Action {} // In validator: return switch (action) { case Deposit d -> d.amount() > 0; case Withdraw w -> w.amount() > 0 && hasSigner; }; ``` Switch expressions check exhaustiveness at compile time. All variants of the sealed interface must be covered. The `default ->` branch can be used as a catch-all for uncovered variants, but prefer explicit cases for clarity. ##### instanceof Pattern Matching ```java if (action instanceof Deposit d) { return d.amount() > 0; } ``` ##### For-Each Loop ```java boolean found = false; for (var item : list) { if (item == target) { found = true; } } ``` Desugared to a recursive fold. Supports single/multi-accumulator, break, and nesting. See [For-Loop Patterns](/guides/for-loop-patterns/). ##### While Loop ```java BigInteger sum = 0; BigInteger i = 0; while (i < 10) { sum = sum + i; i = i + 1; } ``` Desugared to tail-recursive call via Z-combinator. ##### Not Supported C-style `for(;;)`, `do-while`, `try-catch-finally`, `throw`, `continue`. #### Variable Declarations All variables are **immutable** (functional semantics). Reassignment is only supported inside for-each and while loop bodies (accumulator pattern). ```java // OK BigInteger x = 42; var y = x + 1; // NOT OK - assignment not supported x = x + 1; ``` The `var` keyword is supported and types are inferred from the initializer. #### Records Records compile to Constr-encoded Data with fields in declaration order. ```java record VestingDatum(byte[] beneficiary, BigInteger deadline) {} // Construction var datum = new VestingDatum(pkh, 1000); // Field access byte[] b = datum.beneficiary(); BigInteger d = datum.deadline(); ``` #### Helper Methods Static methods in the validator class (without `@Entrypoint`) are compiled as helper functions. Recursive helper methods use Z-combinator transformation. ```java @SpendingValidator class MyValidator { static boolean isPositive(BigInteger x) { return x > 0; } @Entrypoint static boolean validate(BigInteger redeemer, PlutusData ctx) { return isPositive(redeemer); } } ``` #### Lambda Expressions ```java ListsLib.any(list, x -> x > 0) ListsLib.filter(list, x -> Builtins.unIData(x) > 100) ListsLib.foldl((acc, x) -> acc + Builtins.unIData(x), BigInteger.ZERO, list) ``` Lambda bodies can be a single expression or a block: ```java ListsLib.filter(items, item -> { var threshold = new BigInteger("100"); return Builtins.unIData(item) > threshold; }); ``` Equivalent instance method syntax (lambda types auto-inferred): ```java list.any(x -> x.compareTo(BigInteger.ZERO) > 0) list.filter(x -> Builtins.unIData(x).compareTo(BigInteger.valueOf(100)) > 0) // foldl is only available as ListsLib.foldl (no instance method) ``` #### Typed Ledger Access When using `ScriptContext` as a parameter type, the compiler provides typed access to all ledger fields without manual Data decoding. ##### ScriptContext ```java @Entrypoint static boolean validate(MyDatum datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); PlutusData redeemer2 = ctx.redeemer(); ScriptInfo scriptInfo = ctx.scriptInfo(); // ... } ``` ##### TxInfo Fields | Method | Return Type | Field Index | Description | |--------|------------|-------------|-------------| | `txInfo.inputs()` | `List` | 0 | Transaction inputs | | `txInfo.referenceInputs()` | `List` | 1 | Reference inputs | | `txInfo.outputs()` | `List` | 2 | Transaction outputs | | `txInfo.fee()` | `BigInteger` | 3 | Transaction fee (lovelace) | | `txInfo.mint()` | `Value` | 4 | Minted/burned value | | `txInfo.certificates()` | `JulcList` | 5 | Certificates | | `txInfo.withdrawals()` | `JulcMap` | 6 | Reward withdrawals | | `txInfo.validRange()` | `Interval` | 7 | Validity time range | | `txInfo.signatories()` | `JulcList` | 8 | Required signers | | `txInfo.redeemers()` | `JulcMap` | 9 | All redeemers | | `txInfo.datums()` | `JulcMap` | 10 | Datum hash map | | `txInfo.txId()` | `TxId` | 11 | Transaction hash | | `txInfo.votes()` | `JulcMap>` | 12 | Governance votes | | `txInfo.proposalProcedures()` | `JulcList` | 13 | Proposal procedures | | `txInfo.currentTreasuryAmount()` | `Optional` | 14 | Current treasury | | `txInfo.treasuryDonation()` | `Optional` | 15 | Treasury donation | ##### Other Ledger Types - **TxInInfo**: `outRef()` (TxOutRef), `resolved()` (TxOut) - **TxOut**: `address()` (Address), `value()` (Value), `datum()` (OutputDatum), `referenceScript()` (Optional) - **TxOutRef**: `txId()` (byte[]), `outputIndex()` (BigInteger) - **Address**: `credential()` (Credential), `stakingCredential()` (Optional) - **Credential**: sealed interface with `PubKeyCredential(byte[] hash)` and `ScriptCredential(byte[] hash)` - **OutputDatum**: sealed interface with `NoOutputDatum`, `OutputDatumHash(byte[])`, `OutputDatum(PlutusData)` - **ScriptInfo**: sealed interface with `MintingScript(byte[])`, `SpendingScript(TxOutRef, Optional)`, `RewardingScript(Credential)`, `CertifyingScript(BigInteger, TxCert)`, `VotingScript(Voter)`, `ProposingScript(BigInteger, ProposalProcedure)` - **Interval**: `from()` (IntervalBound), `to()` (IntervalBound) - **IntervalBound**: `boundType()` (IntervalBoundType), `isInclusive()` (boolean) - **IntervalBoundType**: sealed with `NegInf`, `Finite(BigInteger)`, `PosInf` ##### Chained Calls ```java // Full typed chain from context to list methods boolean hasSigner = ctx.txInfo().signatories().contains(datum.beneficiary()); ``` #### Instance Methods ##### BigInteger Instance Methods | Method | Return Type | UPLC Translation | |--------|------------|-----------------| | `.abs()` | `BigInteger` | `IfThenElse(x < 0, 0 - x, x)` | | `.negate()` | `BigInteger` | `SubtractInteger(0, x)` | | `.max(other)` | `BigInteger` | `IfThenElse(a < b, b, a)` | | `.min(other)` | `BigInteger` | `IfThenElse(a <= b, a, b)` | | `.equals(other)` | `boolean` | `EqualsInteger` | | `.add(other)` | `BigInteger` | `AddInteger` | | `.subtract(other)` | `BigInteger` | `SubtractInteger` | | `.multiply(other)` | `BigInteger` | `MultiplyInteger` | | `.divide(other)` | `BigInteger` | `DivideInteger` | | `.remainder(other)` | `BigInteger` | `RemainderInteger` | | `.mod(other)` | `BigInteger` | `ModInteger` | | `.signum()` | `BigInteger` | `IfThenElse` chain | | `.compareTo(other)` | `BigInteger` | `IfThenElse` chain | | `.intValue()` | `int` | Identity | | `.longValue()` | `long` | Identity | ##### List Instance Methods | Method | Return Type | Description | |--------|------------|-------------| | `.isEmpty()` | `boolean` | Returns true if empty | | `.size()` | `BigInteger` | Returns the number of elements | | `.head()` | `T` | Returns the first element (decoded) | | `.tail()` | `List` | Returns the list without the first element | | `.get(index)` | `T` | Returns element at index (decoded) | | `.contains(target)` | `boolean` | Recursive search with type-aware equality | | `.reverse()` | `List` | Returns a reversed copy | | `.concat(other)` | `List` | Concatenates two lists | | `.take(n)` | `List` | Returns first n elements | | `.drop(n)` | `List` | Returns list after dropping first n elements | | `.prepend(elem)` | `List` | Prepends element with auto-wrap (BigInteger->IData, byte[]->BData, etc.) | | `.map(f)` | `JulcList` | Apply function to each element (wraps results to Data) | | `.filter(pred)` | `JulcList` | Keep elements matching predicate | | `.any(pred)` | `boolean` | True if any element matches | | `.all(pred)` | `boolean` | True if all elements match | | `.find(pred)` | `T` | First matching element (error if none) | Chaining is supported: `sigs.tail().isEmpty()`, `sigs.tail().contains(pkh)`. `foldl` is only available as a static call (`ListsLib.foldl`), not as an instance method. ##### Map Instance Methods | Method | Return Type | Description | |--------|------------|-------------| | `.get(key)` | `Optional` | Lookup value by key | | `.containsKey(key)` | `boolean` | Check if key exists | | `.size()` | `BigInteger` | Number of entries | | `.isEmpty()` | `boolean` | True if no entries | | `.keys()` | `List` | All keys as list | | `.values()` | `List` | All values as list | | `.insert(key, value)` | `Map` | Insert/update entry (returns pair list) | | `.delete(key)` | `Map` | Remove entry (returns pair list) | MapType variables always hold pair lists internally. `insert` and `delete` return pair lists (not MapData-wrapped). ##### Value Instance Methods | Method | Return Type | Description | |--------|------------|-------------| | `.lovelaceOf()` | `BigInteger` | Extract ADA amount | | `.isEmpty()` | `boolean` | True if value is empty | | `.assetOf(policy, token)` | `BigInteger` | Extract specific token amount | **Caveat**: `value.assetOf(policyId, tokenName)` uses `EqualsData` internally. If `policyId`/`tokenName` are `byte[]` (ByteStringType), wrap with `Builtins.bData()` before passing. ##### Pair Instance Methods | Method | Return Type | Description | |--------|------------|-------------| | `.key()` | `K` | First element with auto-decode | | `.value()` | `V` | Second element with auto-decode | ##### String Instance Methods | Method | Return Type | UPLC Translation | |--------|------------|-----------------| | `.equals(other)` | `boolean` | `EqualsString` | | `.length()` | `BigInteger` | `LengthOfByteString(EncodeUtf8(s))` | ##### ByteString (byte[]) Instance Methods | Method | Return Type | UPLC Translation | |--------|------------|-----------------| | `.equals(other)` | `boolean` | `EqualsByteString` | | `.length()` | `BigInteger` | `LengthOfByteString` | | `.length` | `BigInteger` | `LengthOfByteString` (field access form) | | `.hash()` | `byte[]` | `UnBData` (for list iteration context) | ##### Optional Instance Methods | Method | Return Type | Description | |--------|------------|-------------| | `.isPresent()` | `boolean` | True if Some (tag == 0) | | `.isEmpty()` | `boolean` | True if None (tag == 1) | | `.get()` | `T` | Unwrap the inner value (decoded) | ##### PlutusData Equality Raw `PlutusData` variables support `.equals()` and `==`/`!=` operators using `EqualsData`. #### Standard Library Reference Import from `com.bloxbean.cardano.julc.stdlib.lib.*` in validators. See [Standard Library Guide](/stdlib/stdlib-guide/) for comprehensive documentation. ##### ContextsLib | Method | Args | Description | |--------|------|-------------| | `signedBy(txInfo, pkh)` | TxInfo, ByteString | Check if pkh is in signatories | | `getTxInfo(ctx)` | ScriptContext | Extract TxInfo (legacy — prefer `ctx.txInfo()`) | | `getRedeemer(ctx)` | ScriptContext | Extract redeemer | | `getSpendingDatum(ctx)` | ScriptContext | Extract optional spending datum | | `txInfoInputs(txInfo)` | TxInfo | Get inputs list | | `txInfoOutputs(txInfo)` | TxInfo | Get outputs list | | `txInfoSignatories(txInfo)` | TxInfo | Get signatories list | | `txInfoValidRange(txInfo)` | TxInfo | Get validity time range | | `txInfoMint(txInfo)` | TxInfo | Get minted/burned value | | `txInfoFee(txInfo)` | TxInfo | Get transaction fee | | `txInfoId(txInfo)` | TxInfo | Get transaction hash | | `txInfoRefInputs(txInfo)` | TxInfo | Get reference inputs | | `txInfoWithdrawals(txInfo)` | TxInfo | Get withdrawals map | | `txInfoRedeemers(txInfo)` | TxInfo | Get redeemers map | | `findOwnInput(ctx)` | ScriptContext | Find the input being validated → `Optional` | | `getContinuingOutputs(ctx)` | ScriptContext | Get outputs to same script → `JulcList` | | `ownInputScriptHash(ctx)` | ScriptContext | Get script hash of own input → `byte[]` | | `findDatum(txInfo, datumHash)` | TxInfo, ByteString | Lookup datum by hash | | `valueSpent(txInfo)` | TxInfo | Total value of all inputs | | `valuePaid(txInfo, address)` | TxInfo, Address | Value paid to an address | | `ownHash(scriptInfo)` | ScriptInfo | Get own script hash | | `scriptOutputsAt(txInfo, hash)` | TxInfo, ByteString | Outputs at script hash | | `listIndex(list, index)` | List, Integer | Get element at index | | `trace(msg)` | String | Emit trace message | ##### ListsLib | Method | Args | Description | |--------|------|-------------| | `isEmpty(list)` | List | Check if list is empty | | `length(list)` | List | Count elements | | `head(list)` | List | First element | | `tail(list)` | List | Rest of list | | `reverse(list)` | List | Reverse a list | | `concat(a, b)` | List, List | Concatenate two lists | | `nth(list, n)` | List, Integer | Get element at index | | `take(list, n)` | List, Integer | First n elements | | `drop(list, n)` | List, Integer | Drop first n elements | | `contains(list, elem)` | List, Data | Check membership (EqualsData) | | `containsInt(list, n)` | List, Integer | Check integer membership | | `containsBytes(list, bs)` | List, ByteString | Check bytestring membership | | `hasDuplicateInts(list)` | List | Check for duplicate integers | | `hasDuplicateBytes(list)` | List | Check for duplicate bytestrings | | `empty()` | (none) | Create empty list | | `prepend(elem, list)` | Data, List | Prepend element to list | | `any(list, pred)` | List, Lambda | True if any element matches | | `all(list, pred)` | List, Lambda | True if all elements match | | `find(list, pred)` | List, Lambda | Find first matching (Optional) | | `foldl(f, init, list)` | Lambda, init, List | Left fold | | `map(list, f)` | List, Lambda | Transform elements | | `filter(list, pred)` | List, Lambda | Keep matching elements | | `zip(a, b)` | List, List | Pair elements from two lists | ##### ValuesLib | Method | Args | Description | |--------|------|-------------| | `geqMultiAsset(a, b)` | Value, Value | Multi-asset >= comparison | | `leq(a, b)` | Value, Value | Multi-asset <= comparison | | `eq(a, b)` | Value, Value | Multi-asset equality | | `isZero(value)` | Value | Check if value is zero | | `singleton(policy, token, amount)` | BS, BS, Integer | Create single-token value | | `negate(value)` | Value | Negate all amounts | | `flatten(value)` | Value | Flatten to list of triples | | `flattenTyped(value)` | Value | Flatten to typed `JulcList` | | `add(a, b)` | Value, Value | Add two values | | `subtract(a, b)` | Value, Value | Subtract values | | `countTokensWithQty(mint, policy, qty)` | Value, BS, Integer | Count tokens with exact quantity under policy | | `findTokenName(mint, policy, qty)` | Value, BS, Integer | Find token name with exact quantity under policy | ##### MapLib | Method | Args | Description | |--------|------|-------------| | `lookup(map, key)` | Map, Data | Lookup value by key | | `member(map, key)` | Map, Data | Check key membership | | `insert(map, key, value)` | Map, Data, Data | Insert/update entry | | `delete(map, key)` | Map, Data | Remove entry by key | | `keys(map)` | Map | Get all keys | | `values(map)` | Map | Get all values | | `toList(map)` | Map | Convert to pair list | | `fromList(list)` | List | Convert pair list to map | | `size(map)` | Map | Count entries | ##### OutputLib | Method | Args | Description | |--------|------|-------------| | `txOutAddress(txOut)` | TxOut | Get output address | | `txOutValue(txOut)` | TxOut | Get output value | | `txOutDatum(txOut)` | TxOut | Get output datum | | `outputsAt(outputs, address)` | List, Address | Filter outputs by address | | `countOutputsAt(outputs, addr)` | List, Address | Count outputs at address | | `uniqueOutputAt(outputs, addr)` | List, Address | Get exactly one output at address | | `outputsWithToken(outs, pol, tn)` | List, BS, BS | Filter by token | | `valueHasToken(val, pol, tn)` | Value, BS, BS | Check if value has token | | `lovelacePaidTo(outputs, addr)` | List, Address | Total lovelace to address | | `paidAtLeast(outs, addr, min)` | List, Address, Integer | Check minimum payment | | `getInlineDatum(txOut)` | TxOut | Get inline datum | | `resolveDatum(txOut, datumsMap)` | TxOut, Map | Resolve datum (inline or by hash) | | `findOutputWithToken(outputs, scriptHash, policy, token)` | List, BS, BS, BS | Find output at script address with specific token | | `findInputWithToken(inputs, scriptHash, policy, token)` | List, BS, BS, BS | Find input at script address with specific token | ##### MathLib | Method | Args | Description | |--------|------|-------------| | `abs(x)` | Integer | Absolute value | | `max(a, b)` | Integer, Integer | Maximum | | `min(a, b)` | Integer, Integer | Minimum | | `pow(base, exp)` | Integer, Integer | Exponentiation | | `sign(x)` | Integer | Sign (-1, 0, or 1) | | `divMod(a, b)` | Integer, Integer | Returns Tuple2(quotient, remainder) | | `quotRem(a, b)` | Integer, Integer | Returns Tuple2(quotient, remainder) | | `expMod(base, exp, mod)` | Integer, Integer, Integer | Modular exponentiation | ##### IntervalLib | Method | Args | Description | |--------|------|-------------| | `between(interval, lower, upper)` | Interval, Integer, Integer | Check if interval is within bounds | | `never()` | (none) | Empty interval | | `isEmpty(interval)` | Interval | Check if interval is empty | | `finiteUpperBound(interval)` | Interval | Extract upper bound (if finite) | | `finiteLowerBound(interval)` | Interval | Extract lower bound (if finite) | ##### CryptoLib | Method | Args | Description | |--------|------|-------------| | `verifyEcdsaSecp256k1(key, msg, sig)` | BS, BS, BS | Verify ECDSA secp256k1 signature | | `verifySchnorrSecp256k1(key, msg, sig)` | BS, BS, BS | Verify Schnorr secp256k1 signature | | `ripemd_160(bs)` | BS | RIPEMD-160 hash | Note: `sha2_256`, `sha3_256`, `blake2b_256`, `blake2b_224`, `keccak_256`, and `verifyEd25519Signature` are available directly via `Builtins.*`. ##### ByteStringLib | Method | Args | Description | |--------|------|-------------| | `take(bs, n)` | BS, Integer | Take first n bytes | | `lessThan(a, b)` | BS, BS | Lexicographic less-than | | `lessThanEquals(a, b)` | BS, BS | Lexicographic less-than-or-equal | | `integerToByteString(be, w, i)` | Bool, Integer, Integer | Integer to byte string | | `byteStringToInteger(be, bs)` | Bool, BS | Byte string to integer | | `at(bs, index)` | BS, Integer | Get byte at index | | `cons(byte_, bs)` | Integer, BS | Prepend a byte | | `slice(bs, start, length)` | BS, Integer, Integer | Extract a slice | | `length(bs)` | BS | Length of bytestring | | `drop(bs, n)` | BS, Integer | Drop first n bytes | | `append(a, b)` | BS, BS | Concatenate two bytestrings | | `empty()` | (none) | Empty bytestring | | `zeros(n)` | Integer | Bytestring of n zero bytes | | `equals(a, b)` | BS, BS | Equality check | | `encodeUtf8(s)` | String | Encode string to UTF-8 bytes | | `decodeUtf8(bs)` | BS | Decode UTF-8 bytes to string | | `serialiseData(d)` | Data | Serialize data to CBOR bytes | | `hexNibble(n)` | Integer | Convert nibble (0-15) to hex ASCII code | | `toHex(bs)` | BS | Convert bytestring to hex-encoded bytestring | | `intToDecimalString(n)` | Integer | Convert integer to decimal digit bytestring | | `utf8ToInteger(bs)` | BS | Parse UTF-8 decimal string to integer | ##### BitwiseLib | Method | Args | Description | |--------|------|-------------| | `andByteString(pad, a, b)` | Bool, BS, BS | Bitwise AND | | `orByteString(pad, a, b)` | Bool, BS, BS | Bitwise OR | | `xorByteString(pad, a, b)` | Bool, BS, BS | Bitwise XOR | | `complementByteString(bs)` | BS | Bitwise complement | | `readBit(bs, index)` | BS, Integer | Read single bit | | `writeBits(bs, indices, val)` | BS, List, Bool | Write bits at indices | | `shiftByteString(bs, n)` | BS, Integer | Shift by n bits | | `rotateByteString(bs, n)` | BS, Integer | Rotate by n bits | | `countSetBits(bs)` | BS | Count set (1) bits | | `findFirstSetBit(bs)` | BS | Index of first set bit | ##### AddressLib | Method | Args | Description | |--------|------|-------------| | `credentialHash(cred)` | Credential | Extract hash from credential | | `isScriptAddress(addr)` | Address | True if script address | | `isPubKeyAddress(addr)` | Address | True if pub key address | | `paymentCredential(addr)` | Address | Extract payment credential | ##### BlsLib BLS12-381 elliptic curve operations. Available on PV10+. See [Standard Library Guide](/stdlib/stdlib-guide/#blslib----bls12-381-curve-operations) for full documentation. | Method | Args | Description | |--------|------|-------------| | `g1Add(a, b)` | G1, G1 | Add two G1 elements | | `g1Neg(a)` | G1 | Negate a G1 element | | `g1ScalarMul(scalar, g1)` | Integer, G1 | Scalar multiplication of G1 | | `g1Equal(a, b)` | G1, G1 | Check G1 equality | | `g1Compress(g1)` / `g1Uncompress(bs)` | G1 / BS | G1 compression | | `g1HashToGroup(msg, dst)` | BS, BS | Hash to G1 | | `g2Add`, `g2Neg`, `g2ScalarMul`, `g2Equal`, `g2Compress`, `g2Uncompress`, `g2HashToGroup` | — | G2 equivalents | | `millerLoop(g1, g2)` | G1, G2 | Compute Miller loop pairing | | `mulMlResult(a, b)` | ML, ML | Multiply two Miller loop results | | `finalVerify(a, b)` | ML, ML | Final pairing verification | | `g1MultiScalarMul(scalars, points)` | List, List | Multi-scalar multiplication on G1 | | `g2MultiScalarMul(scalars, points)` | List, List | Multi-scalar multiplication on G2 | ##### NativeValueLib (PV11) > **Protocol Version 11+ only.** Native MaryEra Value operations via CIP-153 builtins. For PV10 networks, use `ValuesLib`. See [Standard Library Guide](/stdlib/stdlib-guide/#nativevaluelib----native-value-operations-pv11) for full documentation. | Method | Args | Description | |--------|------|-------------| | `insertCoin(policy, token, amount, value)` | BS, BS, Integer, Value | Insert/update token quantity | | `lookupCoin(policy, token, value)` | BS, BS, Value | Look up token quantity (0 if absent) | | `union(a, b)` | Value, Value | Merge two Values by adding quantities | | `contains(a, b)` | Value, Value | Check a >= b element-wise | | `scale(scalar, value)` | Integer, Value | Scale all quantities | | `fromData(mapData)` | Data | Convert Map-encoded PlutusData to native Value | | `toData(value)` | Value | Convert native Value back to Map encoding | #### Annotations Reference ##### Validator Annotations | Annotation | Target | Description | |-----------|--------|-------------| | `@SpendingValidator` | Class | Single-purpose spending validator | | `@MintingValidator` | Class | Single-purpose minting validator | | `@WithdrawValidator` | Class | Single-purpose withdrawal validator | | `@CertifyingValidator` | Class | Single-purpose certifying validator | | `@VotingValidator` | Class | Single-purpose voting validator | | `@ProposingValidator` | Class | Single-purpose proposing validator | | `@MultiValidator` | Class | Multi-purpose validator (handles multiple script purposes) | | `@Entrypoint` | Method | Marks the validator entrypoint method | | `@Entrypoint(purpose = Purpose.MINT)` | Method | Purpose-specific entrypoint for multi-validators | | `@Param` | Field | Parameterized field applied at deployment | | `@OnchainLibrary` | Class | Reusable library class (auto-discovered from classpath) | | `@NewType` | Class | Zero-cost type alias for single-field records | ##### Purpose Enum The `Purpose` enum controls dispatch in `@MultiValidator` classes: | Value | ScriptInfo Tag | Description | |-------|---------------|-------------| | `Purpose.DEFAULT` | — | Manual dispatch (user switches on ScriptInfo) | | `Purpose.MINT` | 0 | MintingScript | | `Purpose.SPEND` | 1 | SpendingScript | | `Purpose.WITHDRAW` | 2 | RewardingScript | | `Purpose.CERTIFY` | 3 | CertifyingScript | | `Purpose.VOTE` | 4 | VotingScript | | `Purpose.PROPOSE` | 5 | ProposingScript | #### Testing Utilities ##### JulcEval Type-safe evaluator for testing individual on-chain methods without a full ScriptContext. **Factory methods:** | Method | Description | |--------|-------------| | `JulcEval.forClass(Class)` | Load source from `src/main/java` | | `JulcEval.forClass(Class, Path)` | Load source from custom root | | `JulcEval.forSource(String)` | Use inline Java source | **Proxy mode:** ```java var proxy = JulcEval.forClass(MyHelper.class).create(MyInterface.class); ``` **Fluent call mode:** ```java var result = JulcEval.forClass(MyHelper.class).call("methodName", arg1, arg2); ``` **CallResult extraction:** | Method | Return Type | |--------|------------| | `.asInteger()` | `BigInteger` | | `.asLong()` | `long` | | `.asInt()` | `int` | | `.asByteString()` | `byte[]` | | `.asBoolean()` | `boolean` | | `.asString()` | `String` | | `.asData()` | `PlutusData` | | `.asOptional()` | `Optional` | | `.asList()` | `List` | | `.as(Class)` | `T` | | `.auto()` | `Object` | | `.rawTerm()` | `Term` | **Supported argument types:** `BigInteger`, `int`, `long`, `boolean`, `byte[]`, `String`, `PlutusData`, `PlutusDataConvertible` #### Complete Example ```java import com.bloxbean.cardano.julc.onchain.annotation.*; import com.bloxbean.cardano.julc.onchain.ledger.*; import com.bloxbean.cardano.julc.core.PlutusData; import java.math.BigInteger; @SpendingValidator class VestingValidator { record VestingDatum(byte[] beneficiary, BigInteger deadline) {} @Entrypoint static boolean validate(VestingDatum datum, PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); boolean hasSigner = txInfo.signatories().contains(datum.beneficiary()); boolean pastDeadline = datum.deadline() > 0; return hasSigner && pastDeadline; } } ``` --- ## JuLC Library Developer Guide Source: https://julc.dev/reference/library-developer-guide/ > JuLC Library Developer Guide - JuLC documentation This guide explains how to write, publish, and test on-chain libraries for JuLC, the Java-to-UPLC compiler for Cardano smart contracts. #### 1. Introduction On-chain libraries in JuLC are reusable modules of logic that compile from Java source to UPLC (Untyped Plutus Lambda Calculus) and execute on the Cardano blockchain. When a validator calls a library method such as `MathLib.abs(x)`, the compiler: 1. Discovers the library's Java source file (from the classpath or the same project). 2. Compiles that source to PIR (Plutus Intermediate Representation) alongside the validator. 3. Inlines the library's compiled UPLC code into the final script. Library methods do not execute on the JVM during compilation. They are compiled to UPLC terms that run on-chain inside the Plutus VM. There are two approaches to writing library functions: | Approach | When to use | Complexity | |----------|-------------|------------| | **Java Source (`@OnchainLibrary`)** | Most cases: arithmetic, data traversal, comparisons, builtin wrappers | Low | | **PIR API (programmatic term building)** | Higher-order functions, complex recursion, lambda parameters | High | The vast majority of library functions should use Approach 1. Approach 2 is only needed for patterns that the Java-subset compiler cannot express (primarily higher-order functions that accept function arguments). --- #### 2. Approach 1: Java Source Libraries (`@OnchainLibrary`) This is the primary and recommended approach. You write normal-looking Java static methods, annotate the class with `@OnchainLibrary`, and the JuLC compiler handles the rest. ##### 2.1 The `@OnchainLibrary` Annotation The `@OnchainLibrary` annotation (defined in `julc-stdlib`) marks a class whose static methods can be called from `@SpendingValidator` (or other validator annotation) classes and from other `@OnchainLibrary` classes. ```java package com.bloxbean.cardano.julc.stdlib.annotation; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface OnchainLibrary { } ``` **Source:** `julc-stdlib/src/main/java/com/bloxbean/cardano/julc/onchain/annotation/OnchainLibrary.java` ##### 2.2 Basic Structure A library class must: - Be annotated with `@OnchainLibrary`. - Contain only `public static` methods. - Follow the supported Java subset (see Section 2.5 for details). Here is `MathLib`, the simplest real library in the codebase: ```java package com.bloxbean.cardano.julc.stdlib.lib; import com.bloxbean.cardano.julc.core.PlutusData; import com.bloxbean.cardano.julc.stdlib.annotation.OnchainLibrary; import com.bloxbean.cardano.julc.stdlib.Builtins; @OnchainLibrary public class MathLib { public static long abs(long x) { if (x < 0) { return 0 - x; } else { return x; } } public static long max(long a, long b) { if (a < b) { return b; } else { return a; } } public static long min(long a, long b) { if (a <= b) { return a; } else { return b; } } public static long pow(long base, long exp) { var result = 1L; var e = exp; while (e > 0) { result = result * base; e = e - 1; } return result; } public static long sign(long x) { if (x < 0) { return 0 - 1; } else { if (x == 0) { return 0; } else { return 1; } } } } ``` **Source:** `julc-stdlib/src/main/java/com/bloxbean/cardano/julc/stdlib/lib/MathLib.java` Key observations: - Pure functions, no state. - Only `if/else` and `while` control flow. - Negation is expressed as `0 - x` (unary minus is not supported). - The `var` keyword is used for local variables. ##### 2.3 Using `Builtins.*` for UPLC Primitives The `Builtins` class (in `julc-stdlib`) provides Java method signatures that map directly to UPLC builtin operations. On-chain, calls to these methods are replaced by their corresponding UPLC builtins. Off-chain, the JVM implementations provide executable behavior for testing. All data flowing through the Plutus VM is `PlutusData`. The `Builtins` class provides encode/decode functions to convert between Java types and `PlutusData`: ```java // Encoding to Data Builtins.iData(42) // long -> IntData Builtins.bData(bs) // BytesData -> BytesData (identity wrapper) Builtins.constrData(0, fields) // tag + list-of-fields -> Constr Builtins.listData(list) // list -> ListData Builtins.mapData(pairList) // pair-list -> MapData // Decoding from Data Builtins.unIData(data) // IntData -> long Builtins.unBData(data) // BytesData -> BytesData Builtins.unConstrData(data) // Constr -> (tag, fields) pair Builtins.unListData(data) // ListData -> list Builtins.unMapData(data) // MapData -> pair-list // List primitives Builtins.headList(list) // first element Builtins.tailList(list) // all but first Builtins.nullList(list) // is empty? Builtins.mkCons(elem, list) // prepend element Builtins.mkNilData() // empty data list Builtins.mkNilPairData() // empty pair list // Pair primitives Builtins.fstPair(pair) // first of pair Builtins.sndPair(pair) // second of pair Builtins.mkPairData(a, b) // create a pair // Data decomposition Builtins.constrTag(data) // extract constructor tag (shortcut for FstPair(UnConstrData(data))) Builtins.constrFields(data) // extract constructor fields (shortcut for SndPair(UnConstrData(data))) // Comparison Builtins.equalsData(a, b) // structural equality // Error/Trace Builtins.error() // abort execution Builtins.trace(msg, val) // trace message, return val ``` **Source:** `julc-stdlib/src/main/java/com/bloxbean/cardano/julc/onchain/stdlib/Builtins.java` Here is a real example from `CryptoLib` -- the simplest pattern, where library methods are thin wrappers around builtins: ```java @OnchainLibrary public class CryptoLib { public static PlutusData sha2_256(PlutusData bs) { return Builtins.sha2_256(bs); } public static PlutusData blake2b_256(PlutusData bs) { return Builtins.blake2b_256(bs); } public static boolean verifyEd25519Signature(PlutusData key, PlutusData msg, PlutusData sig) { return Builtins.verifyEd25519Signature(key, msg, sig); } } ``` **Source:** `julc-stdlib/src/main/java/com/bloxbean/cardano/julc/stdlib/lib/CryptoLib.java` ##### 2.4 Complete Example: A Custom `TokenUtils` Library Here is an example of a custom library that checks whether a `Value` contains a specific token and retrieves its amount. This demonstrates real patterns found in `ValuesLib`: ```java package com.example.myproject; import com.bloxbean.cardano.julc.core.PlutusData; import com.bloxbean.cardano.julc.stdlib.annotation.OnchainLibrary; import com.bloxbean.cardano.julc.stdlib.Builtins; @OnchainLibrary public class TokenUtils { /** * Returns the amount of a specific token in a Value. * Returns 0 if the policy/token is not found. * * A Value is Map> * (currency symbol -> token name -> amount). */ public static long tokenAmount(PlutusData value, PlutusData policyId, PlutusData tokenName) { var outerPairs = Builtins.unMapData(value); var result = 0L; var current = outerPairs; while (!Builtins.nullList(current)) { var outerPair = Builtins.headList(current); if (Builtins.equalsData(Builtins.fstPair(outerPair), policyId)) { // Found the policy -- search inner map for the token name var innerPairs = Builtins.unMapData(Builtins.sndPair(outerPair)); result = findToken(innerPairs, tokenName); current = Builtins.mkNilPairData(); // break out of while loop } else { current = Builtins.tailList(current); } } return result; } /** Search an inner token map for a token name. Returns amount or 0. */ public static long findToken(PlutusData innerPairs, PlutusData tokenName) { var result = 0L; var current = innerPairs; while (!Builtins.nullList(current)) { var pair = Builtins.headList(current); if (Builtins.equalsData(Builtins.fstPair(pair), tokenName)) { result = Builtins.unIData(Builtins.sndPair(pair)); current = Builtins.mkNilPairData(); // break } else { current = Builtins.tailList(current); } } return result; } /** * Returns true if the Value contains at least `minAmount` of the given token. */ public static boolean hasToken(PlutusData value, PlutusData policyId, PlutusData tokenName, long minAmount) { var amount = tokenAmount(value, policyId, tokenName); return minAmount <= amount; } } ``` A validator using this library: ```java package com.example.myproject; import java.math.BigInteger; import com.bloxbean.cardano.julc.stdlib.Builtins; @SpendingValidator class TokenGateValidator { @Entrypoint static boolean validate(BigInteger redeemer, PlutusData ctx) { var txInfo = Builtins.headList(Builtins.constrFields(ctx)); var mint = /* extract mint field from txInfo */; var myPolicy = Builtins.bData(/* policy id bytes */); var myToken = Builtins.bData(/* token name bytes */); return TokenUtils.hasToken(mint, myPolicy, myToken, 1); } } ``` ##### 2.5 Supported Java Patterns The JuLC compiler supports a restricted subset of Java. Within `@OnchainLibrary` classes, you can use: **Control Flow:** - `if` / `else` (must always have both branches when returning a value) - `while` loops with accumulator variables - Early exit from `while` by setting the list cursor to an empty list (`Builtins.mkNilData()` or `Builtins.mkNilPairData()`) **Variables:** - `var` declarations with initializers (e.g., `var count = 0L;`) - Variable reassignment only inside `while` and `for-each` loop bodies - No uninitialized variables **Expressions:** - Arithmetic: `+`, `-`, `*`, `/`, `%` - Comparison: `<`, `<=`, `>`, `>=`, `==`, `!=` - Boolean: `&&`, `||`, `!` - Static method calls: `Builtins.headList(x)`, `MyLib.method(a, b)` - Chained calls: `Builtins.unIData(Builtins.sndPair(pair))` **Types:** - `long` for integers - `boolean` for booleans - `PlutusData` for all Plutus data types (lists, maps, constructors, etc.) **The "break" pattern:** Since `break` is not directly supported in UPLC compilation, `while` loops simulate early exit by replacing the loop cursor with an empty list: ```java // Instead of: while (...) { if (found) break; ... } // Use: var current = list; while (!Builtins.nullList(current)) { if (someCondition) { // "break" -- set cursor to empty list to exit the loop current = Builtins.mkNilData(); } else { current = Builtins.tailList(current); } } ``` This pattern is used throughout the standard library, as seen in `ListsLib.contains`: ```java public static boolean contains(PlutusData list, PlutusData target) { var found = false; var current = list; while (!Builtins.nullList(current)) { if (Builtins.equalsData(Builtins.headList(current), target)) { found = true; current = Builtins.mkNilData(); // break } else { current = Builtins.tailList(current); } } return found; } ``` **Source:** `julc-stdlib/src/main/java/com/bloxbean/cardano/julc/stdlib/lib/ListsLib.java` ##### 2.6 Limitations The following Java features are NOT supported in `@OnchainLibrary` classes: - **No lambdas or `.apply()` on functions in Java source libraries** -- you cannot pass functions as arguments in `@OnchainLibrary` Java source code. However, HOFs are available for user code via PIR-based methods (e.g., `list.map(x -> ...)`, `ListsLib.foldl(...)`). To add HOFs to a library, use the PIR API (Approach 2). - **No assignment expressions** -- all variables are immutable outside of `while`/`for-each` loop bodies. - **No `try`/`catch`** -- errors abort execution via `Builtins.error()`. - **No `null`** -- Plutus has no null concept. - **No object creation** (`new`) -- all data is constructed via `Builtins.*` or `PlutusData` factories. - **No Java arrays or raw collections** -- lists are Plutus builtin lists manipulated through `Builtins.headList`, `Builtins.tailList`, etc. PV11 adds `JulcArray` with `Builtins.listToArray`, `Builtins.indexArray`, `Builtins.lengthOfArray`. - **No `return` inside `while` body** -- accumulate into a variable and return after the loop. - **No `for` loops with ranges** -- use `while` with a counter variable. - **No string operations in library source** -- UPLC Text type cannot be compiled from Java source; use PIR API for `trace`. - **No unary minus** -- write `0 - x` instead of `-x`. ##### 2.7 Cross-Library Calls Libraries can call methods from other `@OnchainLibrary` classes. The compiler automatically resolves dependencies transitively. From `ValuesLib.flatten`: ```java public static PlutusData flatten(PlutusData value) { var result = Builtins.mkNilData(); var outerPairs = Builtins.unMapData(value); var current = outerPairs; while (!Builtins.nullList(current)) { var outerPair = Builtins.headList(current); var policyData = Builtins.fstPair(outerPair); var innerPairs = Builtins.unMapData(Builtins.sndPair(outerPair)); result = flattenPolicy(policyData, innerPairs, result); current = Builtins.tailList(current); } return ListsLib.reverse(result); // <-- cross-library call to ListsLib } ``` **Source:** `julc-stdlib/src/main/java/com/bloxbean/cardano/julc/stdlib/lib/ValuesLib.java` (line 213) Cross-library calls work as long as: 1. The called library is also annotated with `@OnchainLibrary`. 2. The called library's source is discoverable (either in the same project, or bundled via `META-INF/plutus-sources/` in a JAR dependency). 3. The import statement (or same-package reference) is present so the resolver can find the dependency. > **Cross-Library BytesData Param Bug**: When calling a stdlib library method that takes `BytesData`/`MapData` typed parameters from user code, the compiler may skip Data encoding at the call boundary if the caller has a variable of the same type. **Workaround**: Pass `PlutusData` typed variables (not `BytesData`/`MapData`) when calling across library boundaries. See [Troubleshooting](/reference/troubleshooting/) for details. > **@NewType records in library parameters**: `@NewType` records resolve to their underlying primitive type at compile time. When accepting `@NewType` parameters in library methods, the parameter will be the underlying type (e.g., `byte[]` for a `@NewType` wrapping `byte[]`). The `LibrarySourceResolver` handles transitive resolution: if `ValuesLib` calls `ListsLib.reverse`, and `ListsLib` calls `Builtins.headList`, all three are automatically included. --- #### 3. Publishing and Distribution When you build a library project as a JAR, the library's Java source files must be bundled into the JAR so that consuming projects can discover and compile them to UPLC. ##### 3.1 The Gradle Plugin `bundleJulcSources` Task The JuLC Gradle plugin registers a `bundleJulcSources` task that: 1. Scans `src/main/java/` for classes containing `@OnchainLibrary`. 2. Copies each matching `.java` source file into `META-INF/plutus-sources/` under `build/resources/main/`, preserving the package directory structure. 3. Generates an `index.txt` manifest listing all bundled source paths. 4. The `jar` task depends on `bundleJulcSources`, so sources are automatically included in the published JAR. **Source:** `julc-gradle-plugin/src/main/java/com/bloxbean/cardano/julc/gradle/BundleJulcSourcesTask.java` Example directory layout in a published JAR: ``` my-library.jar META-INF/ plutus-sources/ index.txt com/ example/ mylib/ TokenUtils.java HelperLib.java ``` ##### 3.2 The `index.txt` Manifest Format The `index.txt` file lists one source file path per line (relative to `META-INF/plutus-sources/`): ``` com/example/mylib/TokenUtils.java com/example/mylib/HelperLib.java ``` This manifest enables reliable source discovery from both file-system directories and JAR archives. **Example from the standard library (`julc-stdlib`):** ``` com/bloxbean/cardano/julc/stdlib/lib/MapLib.java com/bloxbean/cardano/julc/stdlib/lib/MathLib.java com/bloxbean/cardano/julc/stdlib/lib/IntervalLib.java com/bloxbean/cardano/julc/stdlib/lib/CryptoLib.java com/bloxbean/cardano/julc/stdlib/lib/ByteStringLib.java com/bloxbean/cardano/julc/stdlib/lib/BitwiseLib.java com/bloxbean/cardano/julc/stdlib/lib/ContextsLib.java com/bloxbean/cardano/julc/stdlib/lib/ValuesLib.java com/bloxbean/cardano/julc/stdlib/lib/ListsLib.java ``` ##### 3.3 Auto-Discovery from Classpath When the compiler encounters a call to a library method (e.g., `TokenUtils.hasToken(...)`), the `LibrarySourceResolver` discovers the library source using a three-tier strategy: 1. **Tier 1 -- Same-project sources:** Looks for a `.java` file matching the import path under the project's source root directory. 2. **Tier 2 -- Classpath JAR sources:** Scans `META-INF/plutus-sources/index.txt` from all classpath JARs. 3. **Tier 3 -- Transitive resolution:** For each discovered library, recursively resolves its imports until no new libraries are found. **Source:** `julc-compiler/src/main/java/com/bloxbean/cardano/julc/compiler/LibrarySourceResolver.java` ##### 3.4 Setting Up `build.gradle` for a Library Project If you are building a standalone library project (not using the JuLC Gradle plugin), you can replicate the bundling with a custom task. Here is the pattern used by `julc-stdlib`: ```groovy plugins { id 'java-library' } dependencies { api 'com.bloxbean.cardano:julc-core:' implementation 'com.bloxbean.cardano:julc-stdlib:' } // Bundle @OnchainLibrary Java sources into META-INF/plutus-sources/ def generatedResDir = file("${buildDir}/generated/plutus-resources") tasks.register('bundlePlutusSources') { def srcDir = file('src/main/java') def outDir = file("${generatedResDir}/META-INF/plutus-sources") inputs.dir(srcDir) outputs.dir(generatedResDir) doLast { def entries = [] fileTree(srcDir).matching { include '**/*.java' }.each { File f -> if (f.text =~ /(?m)^@OnchainLibrary/) { def relative = srcDir.toPath().relativize(f.toPath()).toString() def target = outDir.toPath().resolve(relative) target.parent.toFile().mkdirs() target.toFile().text = f.text entries << relative } } // Write index file so classpath scanning works from jar URLs too new File(outDir, 'index.txt').text = entries.join('\n') + '\n' } } sourceSets.main.resources.srcDir generatedResDir processResources.dependsOn bundlePlutusSources ``` **Source:** `julc-stdlib/build.gradle` --- #### 4. Testing Libraries Libraries can be tested in two ways: ##### 4.1 Compile-and-Evaluate Pattern Write a minimal validator that calls the library method, compile it with the library source, and evaluate the resulting UPLC program. This is the primary integration testing approach. ```java class TokenUtilsTest { private final JulcCompiler compiler = new JulcCompiler(); private final JulcVm vm = JulcVm.create(); @Test void hasTokenReturnsTrueWhenPresent() { var libSource = """ import com.bloxbean.cardano.julc.core.PlutusData; import com.bloxbean.cardano.julc.stdlib.annotation.OnchainLibrary; import com.bloxbean.cardano.julc.stdlib.Builtins; @OnchainLibrary public class TokenUtils { public static long tokenAmount(PlutusData value, PlutusData policy, PlutusData token) { // ... implementation ... } public static boolean hasToken(PlutusData value, PlutusData policy, PlutusData token, long minAmount) { var amount = tokenAmount(value, policy, token); return minAmount <= amount; } } """; var validatorSource = """ import java.math.BigInteger; @SpendingValidator class TestValidator { @Entrypoint static boolean validate(BigInteger redeemer, BigInteger ctx) { // Test logic calling TokenUtils return true; } } """; var result = compiler.compile(validatorSource, List.of(libSource)); assertFalse(result.hasErrors(), "Compilation failed: " + result.diagnostics()); var program = result.program(); var evalResult = vm.evaluateWithArgs(program, List.of(mockCtx)); assertTrue(evalResult.isSuccess()); } } ``` ##### 4.2 Using `SourceDiscovery` from `julc-testkit` For library projects that use the `@OnchainLibrary` annotation on real source files, the `SourceDiscovery` utility automates discovery and compilation: ```java import com.bloxbean.cardano.julc.testkit.SourceDiscovery; @Test void testMyValidator() { // Automatically finds MyValidator.java under src/main/java, // resolves its library dependencies, and compiles everything var result = SourceDiscovery.compile(MyValidator.class); // result.program() is ready for VM evaluation } ``` **Source:** `julc-testkit/src/main/java/com/bloxbean/cardano/julc/testkit/SourceDiscovery.java` `SourceDiscovery` performs the three-tier library resolution (same-project, classpath JARs, transitive) automatically. ##### 4.3 Off-Chain Testing with `Builtins` Because `Builtins` methods have JVM implementations, you can also unit-test library logic directly off-chain without the compiler: ```java @Test void testContainsOffChain() { var list = Builtins.mkCons( Builtins.iData(10), Builtins.mkCons(Builtins.iData(20), Builtins.mkNilData())); assertTrue(ListsLib.contains(list, Builtins.iData(20))); assertFalse(ListsLib.contains(list, Builtins.iData(99))); } ``` This works because the `@OnchainLibrary` classes call `Builtins.*` methods whose JVM implementations mirror on-chain behavior. --- #### 5. Approach 2: PIR API (Advanced) Some patterns cannot be expressed in the Java subset -- primarily higher-order functions (functions that accept other functions as arguments). For these cases, you build PIR terms programmatically. ##### 5.1 When to Use the PIR API Use Approach 2 when your library function needs: - **Lambda parameters** -- accepting a function and applying it to elements (e.g., `map`, `filter`, `foldl`, `any`, `all`). - **Complex recursion** -- `LetRec` bindings for recursive definitions. - **UPLC Text type** -- `trace` requires the UPLC Text type, which cannot be compiled from Java source. - **Performance-critical hand-tuned UPLC** -- manual control over the exact UPLC output. In the standard library, only these methods use the PIR API: - `ListsLib.any`, `ListsLib.all`, `ListsLib.find`, `ListsLib.foldl`, `ListsLib.map`, `ListsLib.filter`, `ListsLib.zip` (all HOF -- require lambda parameters) - `ContextsLib.trace` (uses UPLC Text type) - `Math.abs`, `Math.max`, `Math.min` (inline PIR delegates for `java.lang.Math`) Everything else is compiled from `@OnchainLibrary` Java source. ##### 5.2 PirTerm Building Blocks All UPLC code is constructed from these PIR term types: | PirTerm | Description | Example | |---------|-------------|---------| | `Var(name, type)` | Variable reference | `new PirTerm.Var("x", new PirType.IntegerType())` | | `Const(constant)` | Literal value | `new PirTerm.Const(Constant.integer(BigInteger.ZERO))` | | `Builtin(fun)` | UPLC builtin function | `new PirTerm.Builtin(DefaultFun.AddInteger)` | | `App(function, arg)` | Function application | `new PirTerm.App(fun, arg)` | | `Lam(param, type, body)` | Lambda abstraction | `new PirTerm.Lam("x", type, body)` | | `Let(name, value, body)` | Let binding | `new PirTerm.Let("x", expr, body)` | | `LetRec(bindings, body)` | Recursive let (for loops) | See `foldl` example below | | `IfThenElse(cond, then, else)` | Conditional | `new PirTerm.IfThenElse(cond, t, f)` | | `DataConstr(tag, type, fields)` | Data constructor | `new PirTerm.DataConstr(0, type, List.of(f1))` | | `Error(type)` | Runtime error | `new PirTerm.Error(new PirType.DataType())` | | `Trace(msg, value)` | Trace message | `new PirTerm.Trace(msg, val)` | | `Binding(name, body)` | Named binding (for `LetRec`) | `new PirTerm.Binding("go", goBody)` | UPLC builtins are applied one argument at a time (curried): ```java // AddInteger(a, b) -- two arguments applied sequentially new PirTerm.App( new PirTerm.App(new PirTerm.Builtin(DefaultFun.AddInteger), a), b); ``` Common constants: ```java Constant.bool(true) // Bool Constant.integer(BigInteger.valueOf(42)) // Integer Constant.integer(BigInteger.ZERO) // Integer 0 Constant.byteString(new byte[]{}) // ByteString (empty) Constant.unit() // Unit () ``` The UplcGenerator handles force counts automatically -- you do not need to add Force wrappers in PIR. ##### 5.3 Pattern: Simple Builtin Wrapper The simplest PIR method wraps a single UPLC builtin: ```java // From StdlibRegistry.registerBuiltins: reg.register("Builtins", "sha2_256", args -> { requireArgs("Builtins.sha2_256", args, 1); return new PirTerm.App(new PirTerm.Builtin(DefaultFun.Sha2_256), args.get(0)); }); ``` ##### 5.4 Pattern: Data Field Extraction Extract a field from a `Constr`-encoded Data value by index: ```java // From StdlibRegistry: Builtins.constrTag extracts FstPair(UnConstrData(data)) reg.register("Builtins", "constrTag", args -> { requireArgs("Builtins.constrTag", args, 1); var unconstr = new PirTerm.App( new PirTerm.Builtin(DefaultFun.UnConstrData), args.get(0)); return new PirTerm.App(new PirTerm.Builtin(DefaultFun.FstPair), unconstr); }); ``` **Source:** `julc-stdlib/src/main/java/com/bloxbean/cardano/julc/stdlib/StdlibRegistry.java` (lines 382-386) ##### 5.5 Pattern: Recursive List Operation with `LetRec` For operations that traverse a list, use `LetRec` for recursion. Here is `ListsLibHof.foldl` -- a left fold: ```java public static PirTerm foldl(PirTerm f, PirTerm init, PirTerm list) { var accVar = new PirTerm.Var("acc", new PirType.DataType()); var lstVar = new PirTerm.Var("lst", new PirType.ListType(new PirType.DataType())); var goVar = new PirTerm.Var("go", new PirType.FunType( new PirType.DataType(), new PirType.FunType( new PirType.ListType(new PirType.DataType()), new PirType.DataType()))); var nullCheck = new PirTerm.App( new PirTerm.Builtin(DefaultFun.NullList), lstVar); var headExpr = new PirTerm.App( new PirTerm.Builtin(DefaultFun.HeadList), lstVar); var tailExpr = new PirTerm.App( new PirTerm.Builtin(DefaultFun.TailList), lstVar); // f acc (HeadList lst) var fApp = new PirTerm.App(new PirTerm.App(f, accVar), headExpr); // go (f acc (HeadList lst)) (TailList lst) var recurse = new PirTerm.App( new PirTerm.App(goVar, fApp), tailExpr); var ifExpr = new PirTerm.IfThenElse(nullCheck, accVar, recurse); var goBody = new PirTerm.Lam("acc", new PirType.DataType(), new PirTerm.Lam("lst", new PirType.ListType(new PirType.DataType()), ifExpr)); var binding = new PirTerm.Binding("go", goBody); return new PirTerm.LetRec( List.of(binding), new PirTerm.App(new PirTerm.App(goVar, init), list)); } ``` **Source:** `julc-stdlib/src/main/java/com/bloxbean/cardano/julc/stdlib/ListsLibHof.java` (lines 99-123) The recursion pattern: 1. Declare a `goVar` that refers to the recursive function itself. 2. Build the body using `goVar` for recursive calls. 3. Wrap in `LetRec(List.of(binding), App(App(goVar, init), list))`. ##### 5.6 Pattern: HOF with Lambda Parameters Higher-order functions accept lambda (`Lam`) parameters. Here is `ListsLibHof.any`, which uses `foldl` internally: ```java public static PirTerm any(PirTerm list, PirTerm predicate) { var accVar = new PirTerm.Var("acc", new PirType.BoolType()); var xVar = new PirTerm.Var("x", new PirType.DataType()); var predApp = new PirTerm.App(predicate, xVar); var body = new PirTerm.IfThenElse( predApp, new PirTerm.Const(Constant.bool(true)), accVar); var foldFn = new PirTerm.Lam("acc", new PirType.BoolType(), new PirTerm.Lam("x", new PirType.DataType(), body)); return foldl(foldFn, new PirTerm.Const(Constant.bool(false)), list); } ``` **Source:** `julc-stdlib/src/main/java/com/bloxbean/cardano/julc/stdlib/ListsLibHof.java` (lines 28-39) ##### 5.7 Registering PIR Methods in `StdlibRegistry` PIR-based methods must be registered in `StdlibRegistry` so the compiler can find them. Registration maps a `(className, methodName)` pair to a `PirTermBuilder`: ```java private static void registerListsLibHof(StdlibRegistry reg) { reg.register("ListsLib", "any", args -> { requireArgs("ListsLib.any", args, 2); return ListsLibHof.any(args.get(0), args.get(1)); }); reg.register("ListsLib", "foldl", args -> { requireArgs("ListsLib.foldl", args, 3); return ListsLibHof.foldl(args.get(0), args.get(1), args.get(2)); }); // ... more registrations ... } ``` Then add the registration call to `defaultRegistry()`: ```java public static StdlibRegistry defaultRegistry() { var reg = new StdlibRegistry(); registerBuiltins(reg); registerListsLibHof(reg); registerContextsTrace(reg); registerJavaMathDelegates(reg); return reg; } ``` **Source:** `julc-stdlib/src/main/java/com/bloxbean/cardano/julc/stdlib/StdlibRegistry.java` (lines 113-120, 405-440) **Important:** `@OnchainLibrary` Java source methods do NOT need registry entries. Only PIR-based methods need explicit registration. The compiler automatically discovers and compiles `@OnchainLibrary` source files. ##### 5.8 Testing PIR-Based Methods PIR methods are tested by building PIR terms, lowering them to UPLC with `UplcGenerator`, and evaluating via `JulcVm`: ```java class StdlibTest { static JulcVm vm; @BeforeAll static void setUp() { vm = JulcVm.create(); } private EvalResult evalPir(PirTerm pir) { var uplc = new UplcGenerator().generate(pir); return vm.evaluate(Program.plutusV3(uplc)); } private boolean evalBool(PirTerm pir) { var result = evalPir(pir); assertTrue(result.isSuccess()); var term = ((EvalResult.Success) result).resultTerm(); var val = ((Term.Const) term).value(); return ((Constant.BoolConst) val).value(); } @Test void anyWithMatchReturnsTrue() { // Build predicate: \x -> LessThanInteger(7, UnIData(x)) var pred = new PirTerm.Lam("x", new PirType.DataType(), new PirTerm.App( new PirTerm.App( new PirTerm.Builtin(DefaultFun.LessThanInteger), new PirTerm.Const(Constant.integer(BigInteger.valueOf(7)))), new PirTerm.App( new PirTerm.Builtin(DefaultFun.UnIData), new PirTerm.Var("x", new PirType.DataType())))); var list = intDataList(1, 5, 10); // [1, 5, 10] var pir = ListsLibHof.any(list, pred); assertTrue(evalBool(pir)); // 10 > 7 } } ``` **Source:** `julc-stdlib/src/test/java/com/bloxbean/cardano/julc/stdlib/StdlibTest.java` --- #### 6. Builtins.java Reference Complete listing of all `Builtins` methods, grouped by category. Each method maps to a UPLC builtin operation on-chain. Off-chain, the JVM implementation is used for testing. ##### List Operations | Method | Signature | UPLC Builtin | |--------|-----------|-------------| | `headList` | `(PlutusData list) -> PlutusData` | `HeadList` | | `tailList` | `(PlutusData list) -> PlutusData` | `TailList` | | `nullList` | `(PlutusData list) -> boolean` | `NullList` | | `mkCons` | `(PlutusData elem, PlutusData list) -> PlutusData` | `MkCons` | | `mkNilData` | `() -> PlutusData` | `MkNilData` | ##### Pair Operations | Method | Signature | UPLC Builtin | |--------|-----------|-------------| | `fstPair` | `(PlutusData pair) -> PlutusData` | `FstPair` | | `sndPair` | `(PlutusData pair) -> PlutusData` | `SndPair` | | `mkPairData` | `(PlutusData fst, PlutusData snd) -> PlutusData` | `MkPairData` | | `mkNilPairData` | `() -> PlutusData` | `MkNilPairData` | ##### Data Encoding | Method | Signature | UPLC Builtin | |--------|-----------|-------------| | `constrData` | `(long tag, PlutusData fields) -> PlutusData` | `ConstrData` | | `iData` | `(long value) -> PlutusData` | `IData` | | `bData` | `(PlutusData bs) -> PlutusData` | `BData` | | `listData` | `(PlutusData list) -> PlutusData` | `ListData` | | `mapData` | `(PlutusData map) -> PlutusData` | `MapData` | ##### Data Decoding | Method | Signature | UPLC Builtin | |--------|-----------|-------------| | `unConstrData` | `(PlutusData data) -> PlutusData` | `UnConstrData` | | `unIData` | `(PlutusData data) -> long` | `UnIData` | | `unBData` | `(PlutusData data) -> PlutusData` | `UnBData` | | `unListData` | `(PlutusData data) -> PlutusData` | `UnListData` | | `unMapData` | `(PlutusData data) -> PlutusData` | `UnMapData` | ##### Data Decomposition Helpers | Method | Signature | UPLC Equivalent | |--------|-----------|-----------------| | `constrTag` | `(PlutusData data) -> long` | `FstPair(UnConstrData(data))` | | `constrFields` | `(PlutusData data) -> PlutusData` | `SndPair(UnConstrData(data))` | ##### Data Comparison | Method | Signature | UPLC Builtin | |--------|-----------|-------------| | `equalsData` | `(PlutusData a, PlutusData b) -> boolean` | `EqualsData` | ##### ByteString Operations | Method | Signature | UPLC Builtin | |--------|-----------|-------------| | `indexByteString` | `(PlutusData bs, long index) -> long` | `IndexByteString` | | `consByteString` | `(long byte_, PlutusData bs) -> PlutusData` | `ConsByteString` | | `sliceByteString` | `(long start, long length, PlutusData bs) -> PlutusData` | `SliceByteString` | | `lengthOfByteString` | `(PlutusData bs) -> long` | `LengthOfByteString` | | `appendByteString` | `(PlutusData a, PlutusData b) -> PlutusData` | `AppendByteString` | | `equalsByteString` | `(PlutusData a, PlutusData b) -> boolean` | `EqualsByteString` | | `lessThanByteString` | `(PlutusData a, PlutusData b) -> boolean` | `LessThanByteString` | | `lessThanEqualsByteString` | `(PlutusData a, PlutusData b) -> boolean` | `LessThanEqualsByteString` | | `integerToByteString` | `(boolean bigEndian, long width, long i) -> PlutusData` | `IntegerToByteString` | | `byteStringToInteger` | `(boolean bigEndian, PlutusData bs) -> long` | `ByteStringToInteger` | | `encodeUtf8` | `(PlutusData s) -> PlutusData` | `EncodeUtf8` | | `decodeUtf8` | `(PlutusData bs) -> PlutusData` | `DecodeUtf8` | | `serialiseData` | `(PlutusData d) -> PlutusData` | `SerialiseData` | | `replicateByte` | `(long n, long byte_) -> PlutusData` | `ReplicateByte` | | `emptyByteString` | `() -> PlutusData` | Constant `#""` | ##### Cryptographic Operations | Method | Signature | UPLC Builtin | |--------|-----------|-------------| | `sha2_256` | `(PlutusData bs) -> PlutusData` | `Sha2_256` | | `sha3_256` | `(PlutusData bs) -> PlutusData` | `Sha3_256` | | `blake2b_256` | `(PlutusData bs) -> PlutusData` | `Blake2b_256` | | `blake2b_224` | `(PlutusData bs) -> PlutusData` | `Blake2b_224` | | `keccak_256` | `(PlutusData bs) -> PlutusData` | `Keccak_256` | | `ripemd_160` | `(PlutusData bs) -> PlutusData` | `Ripemd_160` | | `verifyEd25519Signature` | `(PlutusData key, PlutusData msg, PlutusData sig) -> boolean` | `VerifyEd25519Signature` | | `verifyEcdsaSecp256k1Signature` | `(PlutusData key, PlutusData msg, PlutusData sig) -> boolean` | `VerifyEcdsaSecp256k1Signature` | | `verifySchnorrSecp256k1Signature` | `(PlutusData key, PlutusData msg, PlutusData sig) -> boolean` | `VerifySchnorrSecp256k1Signature` | ##### Bitwise Operations | Method | Signature | UPLC Builtin | |--------|-----------|-------------| | `andByteString` | `(boolean padding, PlutusData a, PlutusData b) -> PlutusData` | `AndByteString` | | `orByteString` | `(boolean padding, PlutusData a, PlutusData b) -> PlutusData` | `OrByteString` | | `xorByteString` | `(boolean padding, PlutusData a, PlutusData b) -> PlutusData` | `XorByteString` | | `complementByteString` | `(PlutusData bs) -> PlutusData` | `ComplementByteString` | | `readBit` | `(PlutusData bs, long index) -> boolean` | `ReadBit` | | `writeBits` | `(PlutusData bs, PlutusData indices, boolean value) -> PlutusData` | `WriteBits` | | `shiftByteString` | `(PlutusData bs, long n) -> PlutusData` | `ShiftByteString` | | `rotateByteString` | `(PlutusData bs, long n) -> PlutusData` | `RotateByteString` | | `countSetBits` | `(PlutusData bs) -> long` | `CountSetBits` | | `findFirstSetBit` | `(PlutusData bs) -> long` | `FindFirstSetBit` | ##### Math Operations | Method | Signature | UPLC Builtin | |--------|-----------|-------------| | `expModInteger` | `(long base, long exp, long mod) -> long` | `ExpModInteger` | ##### Error and Trace | Method | Signature | UPLC Builtin | |--------|-----------|-------------| | `error` | `() -> PlutusData` | `Error` (aborts execution) | | `trace` | `(String message, PlutusData value) -> PlutusData` | `Trace` | --- #### 7. Checklist for Adding a New Library Function ##### For Java Source Libraries (`@OnchainLibrary`) - [ ] **Write the method** in your `@OnchainLibrary` class under `src/main/java/`. - Use only `public static` methods. - Follow the supported Java subset (Section 2.5). - Use `Builtins.*` for all UPLC primitive operations. - [ ] **Verify the annotation:** Ensure the class has `@OnchainLibrary` at the class level. - [ ] **Check cross-library imports:** If calling methods from other libraries, ensure the import statement is present and the dependency is also an `@OnchainLibrary`. - [ ] **Run the bundle task:** `./gradlew bundlePlutusSources` (or `bundleJulcSources` if using the plugin) to verify the source is picked up. - [ ] **Write integration tests** using the compile-and-evaluate pattern (Section 4.1). Cover: - Normal operation with expected inputs. - Edge cases (empty lists, zero values, boundary conditions). - Error cases where applicable. - [ ] **Test off-chain** with direct `Builtins` calls if appropriate (Section 4.3). - [ ] **Run the full test suite:** `./gradlew test` to verify no regressions. ##### For PIR API Methods (Approach 2) - [ ] **Write the PIR term builder** in a class under `julc-stdlib` (e.g., `ListsLibHof.java`). - Use unique variable name suffixes (e.g., `acc_map`, `x_flt`) to avoid shadowing. - Use `Let` bindings for expressions used more than once. - PirType accuracy matters: use `DataType` for general Data, `IntegerType`/`BoolType` for decoded values, `ListType` for builtin lists. - [ ] **Register in `StdlibRegistry`:** Add a `reg.register(...)` call with `requireArgs` validation. - [ ] **Add the registration call** to `defaultRegistry()` if creating a new registration group. - [ ] **Write PIR-level tests** using `UplcGenerator` and `JulcVm.evaluate` (Section 5.8). - [ ] **Update the registry test** in `StdlibTest.RegistryTests` to verify the new entry is present and the count is correct. - [ ] **Run the full test suite:** `./gradlew test` ##### Data Encoding Reference When constructing test data or library logic, remember these Plutus data encodings: - **Boolean:** `Constr(0, [])` = False, `Constr(1, [])` = True - **Optional:** `Constr(0, [x])` = Some(x), `Constr(1, [])` = None - **Value:** `Map>` -- currency symbol to (token name to amount) - **Lovelace:** Stored under empty bytestring policy and empty bytestring token name - **Pairs:** Encoded as `Constr(0, [fst, snd])` by `MkPairData` --- ## Examples Source: https://julc.dev/reference/examples/ > Example validators demonstrating JuLC features and patterns Explore real-world validator examples from the [julc-examples](https://github.com/bloxbean/julc-examples) repository. Each example demonstrates specific JuLC features and patterns. #### Getting Started Examples Time-locked vesting contract. Demonstrates basic datum, redeemer, and `ScriptContext` usage with beneficiary + deadline checks. One-shot NFT minting. Shows `@MintingValidator`, policy ID validation, and mint quantity checks. #### Advanced Validators Multi-signature authorization. Demonstrates list operations on `signatories`, counting required signatures. Full-featured vesting with interval checking, multiple beneficiaries, and proper deadline validation using `IntervalLib`. Minting policy with authorization. Uses sealed interface redeemer (`Mint`/`Burn`), signatory checks, and mint value validation. UTxO output validation. Demonstrates `OutputLib` for checking values paid to addresses, lovelace amounts, and token presence. #### Conway Governance Staking reward withdrawal. Shows `@WithdrawValidator` annotation and reward-specific `ScriptContext` handling. Governance voting. Demonstrates `@VotingValidator` and Conway-era governance features. #### Testing Patterns Comprehensive test demonstrating all language features: records, sealed interfaces, switch, loops, lambdas, HOFs, stdlib usage. Debugging techniques with `Builtins.trace()`, source maps, and failure diagnostics. Property-based testing with jqwik. Generates random redeemers and checks minting invariants. Property-based testing for vesting logic. Generates random deadlines, beneficiaries, and signatories. #### Quick Links - [julc-helloworld](https://github.com/bloxbean/julc-helloworld) — Minimal starter project - [julc-examples source](https://github.com/bloxbean/julc-examples) — Full source code for all examples above --- ## JuLC Compiler Troubleshooting Guide Source: https://julc.dev/reference/troubleshooting/ > JuLC Compiler Troubleshooting Guide - JuLC documentation This guide maps every compiler, validation, configuration, and runtime error to its cause and solution. All error messages shown here are taken directly from the JuLC source code. --- #### 1. Compilation Errors (PirGenerator) These errors are emitted during PIR (Plutus Intermediate Representation) generation, when the compiler translates Java AST nodes into UPLC-compatible terms. ##### 1.1 `Method must have a body: ` **Cause:** An abstract or interface method was encountered where the compiler expects a concrete method body. **Fix:** Ensure every method in your validator class has a full body. On-chain code cannot contain abstract methods. ```java // WRONG: abstract method @SpendingValidator public class MyValidator { @Entrypoint public abstract boolean validate(Data redeemer, ScriptContext ctx); } // CORRECT: method with body @SpendingValidator public class MyValidator { @Entrypoint public static boolean validate(Data redeemer, ScriptContext ctx) { return true; } } ``` --- ##### 1.2 `Variable must be initialized: ` **Full message:** `Variable must be initialized: . Hint: On-chain variables need initial values, e.g. var = BigInteger.ZERO;` **Cause:** A local variable was declared without an initializer. On-chain code requires all variables to be initialized at declaration because UPLC has no concept of uninitialized memory. **Fix:** Always assign an initial value. ```java // WRONG var total; // CORRECT var total = BigInteger.ZERO; ``` --- ##### 1.3 `break statement outside of a loop` **Full message:** `break statement outside of a loop` **Hint:** `break can only be used inside for-each or while loops.` **Cause:** A `break` statement appeared outside of a for-each or while loop scope. **Fix:** Move the `break` inside a loop body, or restructure your logic to use `return` instead. ```java // WRONG if (someCondition) { break; // not inside a loop } // CORRECT for (var item : items) { if (someCondition) { break; // inside a for-each loop } } ``` --- ##### 1.4 `Unsupported statement: ` **Full message:** `Unsupported statement: ` **Hint:** `Only variable declarations, if/else, for-each, while, return, and expression statements are supported on-chain.` **Cause:** A Java statement type that has no on-chain equivalent was used. Unsupported statements include `switch` statements (as opposed to `switch` expressions), `try`/`catch`, `do`/`while`, labeled statements, and others. **Fix:** Rewrite using the supported statement forms: variable declarations, if/else, for-each, while, return, and expression statements. ```java // WRONG: switch statement switch (value) { case 1: doSomething(); break; } // CORRECT: switch expression (for sealed interface pattern matching) var result = switch (action) { case Bid b -> b.amount(); case Cancel c -> BigInteger.ZERO; }; ``` --- ##### 1.5 `Unsupported statement in break-aware loop body: ` **Hint:** `Inside loops with break, only variable declarations, assignments, if/else, and break are supported.` **Cause:** A loop body that contains `break` used an unsupported statement type. When a loop body contains `break`, the compiler uses a specialized translation path that supports a narrower set of statements. **Fix:** Simplify the loop body to contain only variable declarations, accumulator assignments, if/else, and break. ```java // WRONG: nested for-each inside a break-aware loop body for (var item : items) { for (var sub : item.children()) { // not supported inside break-aware body if (condition) break; } } // CORRECT: nested for-each without break is supported for (var item : items) { for (var sub : item.children()) { // OK — no break in outer loop sum = sum + sub.amount(); } } // CORRECT: use a helper method for the break pattern var found = false; for (var item : items) { if (checkCondition(item)) { found = true; break; } } ``` Note: Nested loops (while-in-while, for-each-in-for-each, mixed) are fully supported. The restriction is specifically about nesting inside a **break-aware** loop body — when the outer loop uses `break`, the inner statements must be limited to declarations, assignments, if/else, and break. --- ##### 1.6 `Unsupported in multi-acc loop body: ` **Hint:** `Inside multi-accumulator loops, only variable declarations, assignments, and if/else are supported.` **Cause:** A for-each or while loop that updates multiple accumulators used an unsupported statement. Multi-accumulator loops are translated by packing all accumulators into a Data list tuple, which restricts the supported constructs. **Fix:** Keep the loop body to variable declarations, accumulator assignments, and if/else. ```java // CORRECT: multi-accumulator loop var count = BigInteger.ZERO; var total = BigInteger.ZERO; for (var item : items) { count = count + BigInteger.ONE; total = total + item.value(); } ``` --- ##### 1.7 `Unsupported in multi-acc break-aware body: ` **Hint:** `Inside multi-accumulator loops with break, only variable declarations, assignments, if/else, and break are supported.` **Cause:** A multi-accumulator loop that also contains `break` used an unsupported statement. This is the most restrictive loop compilation mode. **Fix:** Use only variable declarations, assignments, if/else, and break in the loop body. --- ##### 1.8 `Unsupported BigInteger field: ` **Full message:** `Unsupported BigInteger field: ` **Hint:** `Supported BigInteger fields: ZERO, ONE, TWO, TEN. Use new BigInteger("value") for other constants.` **Cause:** A `BigInteger` static field other than `ZERO`, `ONE`, `TWO`, or `TEN` was referenced. **Fix:** Use `new BigInteger("value")` for arbitrary constants. ```java // WRONG var x = BigInteger.valueOf(42); // valueOf is fine for runtime, but... var y = BigInteger.NEGATIVE_ONE; // this field is not supported // CORRECT var x = BigInteger.valueOf(42); // valueOf IS supported var y = new BigInteger("-1"); // use string constructor for other values ``` --- ##### 1.9 `new BigInteger() requires a string literal argument` **Hint:** `Use new BigInteger("12345") or BigInteger.valueOf(n) for integer constants.` **Cause:** `new BigInteger(expr)` was called with a non-string-literal argument (e.g., a variable). **Fix:** Pass a string literal directly to the `BigInteger` constructor. ```java // WRONG var s = "100"; var amount = new BigInteger(s); // variable, not a string literal // CORRECT var amount = new BigInteger("100"); // string literal var small = BigInteger.valueOf(42); // also works for int-range values ``` --- ##### 1.10 `Cannot construct non-record type: ` **Hint:** `Only record types can be constructed on-chain. Define as a record.` **Cause:** A `new ClassName(...)` expression was used with a type that is not a Java `record` or a variant of a sealed interface. **Fix:** Define your data types as records. ```java // WRONG: regular class public class Bid { public BigInteger amount; public Bid(BigInteger amount) { this.amount = amount; } } // CORRECT: record public record Bid(BigInteger amount) {} ``` --- ##### 1.11 `switch expression requires a sealed interface type, got: ` **Hint:** `Ensure the switch variable's type is a sealed interface with record variants.` **Cause:** A `switch` expression was used on a variable that is not typed as a sealed interface with record variants. **Fix:** Ensure the switched-on variable is a sealed interface with record implementations. ```java // Define the sum type public sealed interface Action permits Bid, Cancel {} public record Bid(BigInteger amount) implements Action {} public record Cancel() implements Action {} // CORRECT usage var result = switch (action) { case Bid b -> b.amount(); case Cancel c -> BigInteger.ZERO; }; ``` --- ##### 1.12 `Unknown variant in switch: ` **Hint:** `Available variants: ` **Cause:** A `case` label in a switch expression referenced a type name that is not a known variant (constructor) of the sealed interface. **Fix:** Check spelling and ensure the type is listed as a `permits` variant of the sealed interface. ```java // WRONG: "Offer" is not a variant of Action var result = switch (action) { case Offer o -> o.price(); // Offer not in permits list case Cancel c -> BigInteger.ZERO; }; // CORRECT var result = switch (action) { case Bid b -> b.amount(); // Bid is a permitted variant case Cancel c -> BigInteger.ZERO; }; ``` --- ##### 1.13 `Unsupported instanceof pattern: ` **Hint:** `instanceof is supported only with sealed interface variants: if (x instanceof Variant v) { ... }` **Cause:** `instanceof` was used with a type that is not a variant of a sealed interface, or the pattern form is not supported. **Fix:** Use `instanceof` only for sealed interface variant matching. ```java // WRONG: instanceof with a non-variant type if (obj instanceof String s) { ... } // CORRECT: instanceof with sealed interface variant if (action instanceof Bid b) { var amount = b.amount(); } ``` --- ##### 1.14 `Non-exhaustive switch on sealed interface: missing cases [X, Y]` **Cause:** A `switch` expression on a sealed interface does not cover all variants. The compiler checks exhaustiveness before generating `DataMatch`. **Fix:** Add explicit cases for all variants of the sealed interface, or use `default ->` as a catch-all for uncovered variants. ```java sealed interface Action permits Bid, Cancel, Update {} record Bid(BigInteger amount) implements Action {} record Cancel() implements Action {} record Update(BigInteger newPrice) implements Action {} // WRONG: missing Update case var result = switch (action) { case Bid b -> b.amount(); case Cancel c -> BigInteger.ZERO; }; // Error: Non-exhaustive switch on sealed interface: missing cases [Update] // CORRECT: all cases covered var result = switch (action) { case Bid b -> b.amount(); case Cancel c -> BigInteger.ZERO; case Update u -> u.newPrice(); }; ``` --- ##### 1.15 `Method does not return on all execution paths` **Cause:** A method has code paths that do not end with a `return` statement. The compiler analyzes if/else completeness, fallthrough returns, and loop-as-return patterns. **Fix:** Ensure every code path returns a value. ```java // WRONG: missing return on else path static boolean check(BigInteger x) { if (x > 0) { return true; } // no return here } // CORRECT static boolean check(BigInteger x) { if (x > 0) { return true; } else { return false; } } ``` --- ##### 1.16 `@NewType requires exactly one field` **Cause:** A record annotated with `@NewType` has zero or more than one field. **Fix:** `@NewType` records must have exactly one field of a supported primitive type (`byte[]`, `BigInteger`, `String`, `boolean`). ```java // WRONG: two fields @NewType record AssetId(byte[] policy, byte[] name) {} // CORRECT: single field @NewType record PolicyId(byte[] hash) {} ``` --- ##### 1.17 `Unsupported lambda body: ` **Hint:** `Lambda bodies must be a single expression or a block statement.` **Cause:** A lambda expression has an unexpected body form. **Fix:** Use either a single expression or a block body. ```java // CORRECT: expression body var doubled = ListsLib.map(items, item -> item * 2); // CORRECT: block body var filtered = ListsLib.filter(items, item -> { var threshold = new BigInteger("100"); return item > threshold; }); ``` --- ##### 1.15 `Unknown method: ` **Hint (fuzzy match):** `Did you mean '()'?` **Hint (no match):** `Library methods require class qualification (e.g., MathLib.(...)).` **Cause:** A method call could not be resolved. The method is not a local helper, a stdlib method, a record accessor, or a known type method. **Fix:** Qualify static library calls with their class name. Check spelling. ```java // WRONG: unqualified stdlib call var len = length(myList); // CORRECT: qualified stdlib call var len = ListsLib.length(myList); // WRONG: typo var total = MathLib.abss(value); // CORRECT: compiler will suggest "Did you mean 'MathLib.abs()'?" var total = MathLib.abs(value); ``` --- ##### 1.16 `Unsupported operator: ` **Hint:** `Supported operators: +, -, *, /, %, ==, !=, <, <=, >, >=, &&, ||` **Cause:** A binary operator that has no UPLC equivalent was used (e.g., bitwise operators `&`, `|`, `^`, `<<`, `>>`). **Fix:** Use the supported operators. For bitwise operations, use `BitwiseLib`. ```java // WRONG var result = a & b; var shifted = x << 2; // CORRECT var result = BitwiseLib.andByteString(false, a, b); var shifted = BitwiseLib.shiftByteString(x, 2); ``` --- ##### 1.17 `Unsupported unary operator: ` **Hint:** `Supported unary operators: ! (logical NOT) and - (negation)` **Cause:** A unary operator other than `!` or `-` was used (e.g., `~`, `++`, `--`). **Fix:** Use `!` for boolean negation and `-` for arithmetic negation. ```java // WRONG var next = count++; var flipped = ~bits; // CORRECT var negated = !condition; var negative = -amount; ``` --- ##### 1.18 `Unsupported expression: AssignExpr` **Hint:** `Variables are immutable on-chain. Use the accumulator pattern in for-each/while loops for mutable state.` **Cause:** An assignment expression was used outside of the accumulator pattern in a loop. On-chain UPLC does not support mutation. **Fix:** Variables are immutable. Use the accumulator pattern in for-each or while loops. ```java // WRONG: assignment outside loop accumulator var x = BigInteger.ZERO; x = BigInteger.ONE; // not inside a loop accumulator context // CORRECT: accumulator pattern in for-each var total = BigInteger.ZERO; for (var item : items) { total = total + item.value(); // accumulator assignment in loop } ``` --- ##### 1.19 `Unsupported expression: ` **Hint:** `This expression type is not supported in on-chain code.` **Cause:** A Java expression type that has no on-chain compilation path was used. **Fix:** Rewrite using the supported expression forms: literals, variables, binary and unary expressions, method calls, field access, record construction, ternary expressions, lambdas, switch expressions, instanceof, and casts. --- ##### 1.20 `Undefined variable: ` **Cause:** A variable name was used that is not in scope. This is thrown by `SymbolTable.require()` when no matching variable definition is found. **Fix:** Ensure the variable is declared before use, or check for typos. ```java // WRONG: typo var totl = BigInteger.ZERO; return total; // "total" not defined, "totl" is // CORRECT var total = BigInteger.ZERO; return total; ``` --- #### 2. Validation Errors (SubsetValidator) These errors are emitted during the subset validation pass, before PIR generation begins. They reject Java constructs that have no on-chain equivalent. Each error includes the file name, line number, and column. ##### 2.1 `try/catch is not supported on-chain` **Suggestion:** `Use if/else checks instead of exception handling` **Cause:** A `try`/`catch`/`finally` block was used. UPLC has no exception handling mechanism. **Fix:** Replace with explicit if/else validation logic. ```java // WRONG try { riskyOperation(); } catch (Exception e) { return false; } // CORRECT if (!isValid(input)) { return false; } ``` --- ##### 2.2 `throw is not supported on-chain` **Suggestion:** `Return false from the validator to reject a transaction` **Cause:** A `throw` statement was used. On-chain validators reject transactions by returning `false`, not by throwing exceptions. **Fix:** Return `false` to reject a transaction. ```java // WRONG if (!authorized) { throw new RuntimeException("Unauthorized"); } // CORRECT if (!authorized) { return false; } ``` --- ##### 2.3 `synchronized is not supported on-chain` **Suggestion:** `On-chain code is single-threaded; remove synchronized blocks` **Cause:** A `synchronized` block or method was used. On-chain execution is single-threaded and deterministic. **Fix:** Remove the `synchronized` keyword. --- ##### 2.4 `C-style for loops are not supported on-chain` **Suggestion:** `Use for-each over a list or while loops instead` **Cause:** A traditional `for (init; cond; update)` loop was used. **Fix:** Use `for-each` over a list or a `while` loop with an accumulator. ```java // WRONG for (int i = 0; i < 10; i++) { // ... } // CORRECT for (var item : items) { // process item } // CORRECT: while loop with accumulator var i = BigInteger.ZERO; while (i < BigInteger.TEN) { // process i = i + BigInteger.ONE; } ``` --- ##### 2.5 `break is only supported inside for-each or while loops on-chain` **Suggestion:** `Use for-each or while with an accumulator and break to exit early` **Cause:** A `break` statement appeared outside of a for-each or while loop. **Fix:** Place `break` inside a for-each or while loop. --- ##### 2.6 `do-while loops are not supported on-chain` **Suggestion:** `Use while loops or for-each instead` **Cause:** A `do { ... } while (cond)` loop was used. **Fix:** Rewrite as a `while` loop. ```java // WRONG do { process(); } while (hasMore); // CORRECT while (hasMore) { process(); } ``` --- ##### 2.7 `null is not supported on-chain` **Suggestion:** `Use Optional to represent absence of a value` **Cause:** The `null` literal was used. UPLC does not have a null concept. **Fix:** Use `Optional` or sentinel values. ```java // WRONG Data result = null; // CORRECT var result = Optional.empty(); ``` --- ##### 2.8 `'this' is not supported on-chain` **Suggestion:** `On-chain validators are stateless; use static methods instead` **Cause:** The `this` keyword was used. On-chain validators are purely functional and stateless. **Fix:** Use static methods and pass all data as parameters. ```java // WRONG public boolean validate() { return this.checkSignature(); } // CORRECT public static boolean validate(ScriptContext ctx) { return checkSignature(ctx); } ``` --- ##### 2.9 `'super' is not supported on-chain` **Suggestion:** `Use sealed interfaces and pattern matching instead of inheritance` **Cause:** The `super` keyword was used. Inheritance is not supported on-chain. **Fix:** Use sealed interfaces with record variants and pattern matching. --- ##### 2.10 `arrays are not supported on-chain` **Suggestion:** `Use List instead of arrays` **Cause:** An array creation expression (e.g., `new int[10]`) was used. **Fix:** Use `List` instead. ```java // WRONG var data = new byte[32]; // CORRECT // Use ByteString for byte arrays, or List for element lists ``` > **PV11 Note:** From protocol version 11, `JulcArray` provides O(1) random-access arrays via CIP-156 builtins. Use `list.toArray()` to convert a list. --- ##### 2.11 `array access is not supported on-chain` **Suggestion:** `Use List with list operations instead` **Cause:** Array indexing (e.g., `arr[i]`) was used. **Fix:** Use list operations from `ListsLib`. ```java // WRONG var first = items[0]; // CORRECT var first = Builtins.headList(items); // or var nth = ListsLib.nth(items, 2); ``` > **PV11 Note:** From protocol version 11, `JulcArray` provides O(1) random-access arrays via CIP-156 builtins. Use `list.toArray()` to convert a list, then `arr.get(index)` for O(1) access. --- ##### 2.12 `floating point types (float/double) are not supported on-chain` **Suggestion:** `Use BigInteger for integer arithmetic or Rational for fractions` **Cause:** A `float` or `double` type was used. UPLC only supports exact integer arithmetic. **Fix:** Use `BigInteger` for integer math. For fractions, use a rational number pattern (numerator/denominator as `BigInteger`). ```java // WRONG double price = 1.5; float rate = 0.03f; // CORRECT var priceNumerator = new BigInteger("3"); var priceDenominator = new BigInteger("2"); // represents 1.5 ``` --- ##### 2.13 `class inheritance is not supported on-chain` **Suggestion:** `Use sealed interfaces with record variants instead` **Cause:** A class extends another class (other than `Object`). **Fix:** Use sealed interfaces with record variants for polymorphism. ```java // WRONG public class SpecialBid extends Bid { ... } // CORRECT public sealed interface Action permits Bid, Cancel {} public record Bid(BigInteger amount) implements Action {} public record Cancel() implements Action {} ``` --- #### 3. Configuration Errors (JulcCompiler) These errors are raised during compiler setup and pipeline orchestration. ##### 3.1 `Failed to parse