Parameterized validators
A parameter is a value baked into a script when you deploy it, such as an admin’s key or a limit. This page shows how to declare parameters, test them, and apply them to the built script with elci apply.
Why parameters
Section titled “Why parameters”A datum belongs to one output; a parameter belongs to the whole script. Two deployments of the same code with different parameters are different scripts, with different hashes, addresses and policy ids. That is exactly what you want when the value decides who controls the script: someone who deploys your policy with their own admin key gets their own policy id, not yours.
Declaring parameters
Section titled “Declaring parameters”List the names under parameters in the validator’s table. The handlers receive them as leading arguments, in that order. This minting policy lets an admin mint between a minimum and a maximum quantity per transaction:
[project]source-directories = ["src"]target = "plutus-v3-pv11-uplc-1.1.0"julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT"
[validators.drop]boundary = "julc-strict-v1"parameters = ["admin", "limits"]mint = "Drop.mint"module Drop exposing (Limits, mint)
import Julc.Ledger exposing (PubKeyHash, ScriptContext)
type alias Limits = { minimum : Int , maximum : Int }
mint : PubKeyHash -> Limits -> Int -> ScriptContext -> Boolmint admin limits quantity ctx = (quantity >= limits.minimum) && (quantity <= limits.maximum) && List.any (\signer -> signer == admin) ctx.txInfo.signatoriesadmin and limits come first, then the redeemer (here the quantity the transaction claims to mint), then the context.
The rules:
-
Order matters. Parameters are applied in the listed order, and every handler of the validator receives all of them, in that order.
-
Types must agree. Each parameter must have the same type in every handler. If
Treasury.spendtakesadmin : PubKeyHashbutTreasury.withdrawdeclares and uses it as anInt, the check fails:.../treasury/src/Treasury.elm:11:1: ELCI2014: Handler Treasury.withdraw does not have the withdraw signature admin -> redeemer -> ScriptContext -> Bool that [validators.treasury] needs (1 parameter); its type is Int -> () -> ScriptContext -> Bool; it has `Int` where the configuration needs `PubKeyHash`11| withdraw : Int -> () -> ScriptContext -> Bool^^^^^^^^In .../treasury/elci.toml:6: [validators.treasury] -
Boundary types only. Parameters follow the same rules as datums and redeemers: records, custom types, lists, dictionaries, maps and ledger types such as
PubKeyHashorTxOutRefare fine; functions, arrays andValueare not. To parameterize a script by an amount of tokens, take aList ( PolicyId, TokenName, Int )and build the value withValue.fromList. See Datums and redeemers. -
Names start with a lowercase letter and use letters, digits and
_.
Parameters are decoded when the script is applied, but the script doesn’t validate them on-chain. elci apply checks them off-chain instead, as shown below. The one exception is a Dict: the script checks its key order on every run, as it does in a datum.
Testing with parameters
Section titled “Testing with parameters”A scenario supplies parameters as Data, in the declared order, with Scenario.parameters:
module DropTest exposing (adminMintsWithinLimits, onlyQuantitiesWithinLimits, tooManyAreRejected)
import Bytesimport Drop exposing (Limits)import Julc.Data as Dataimport Julc.Ledger exposing (PubKeyHash)import Property exposing (Property)import Scenario exposing (Scenario)
admin : PubKeyHashadmin = Scenario.key "admin"
limits : Limitslimits = { minimum = 1, maximum = 10 }
drop : Int -> Scenariodrop quantity = Scenario.mint "drop" |> Scenario.parameters [ Data.encode admin, Data.encode limits ] |> Scenario.minting (Bytes.fromString "DROP") quantity |> Scenario.redeemer (Data.encode quantity) |> Scenario.signedBy admin
adminMintsWithinLimits : ScenarioadminMintsWithinLimits = drop 10
tooManyAreRejected : ScenariotooManyAreRejected = drop 11 |> Scenario.expectFailure
onlyQuantitiesWithinLimits : Int -> PropertyonlyQuantitiesWithinLimits quantity = if quantity >= 1 && quantity <= 10 then Property.scenario (drop quantity)
else Property.scenario (Scenario.expectFailure (drop quantity))elci test . --seed 42PASS DropTest.adminMintsWithinLimits (cpu 6369489, mem 23925)PASS DropTest.tooManyAreRejected (cpu 3787162, mem 15380)PASS DropTest.onlyQuantitiesWithinLimits (100 runs, max cpu 6369489, mem 23925)3 passed, 0 failed (seed 42)The test runner applies the parameters to the compiled script before it builds the transaction, so the script’s address and policy id in each scenario are those of the applied script. A scenario must supply every parameter. If drop passed only [ Data.encode admin ], its scenarios would fail before running:
FAIL DropTest.adminMintsWithinLimits cannot prepare validator 'drop': it takes 2 parameters [admin, limits], but the scenario gives 1; set them with Scenario.parameters0 passed, 1 failedThe built script
Section titled “The built script”elci build writes the script before parameters are applied. Its validator.plutus.json lists the parameters and leaves the hash empty, because the hash only exists once the parameters are known:
{ "type": "PlutusScriptV3", "purpose": "minting", "description": "drop", "cborHex": "...", "hash": "", "params": "admin:PubKeyHash,limits:Record", "sizeBytes": 218}params shows records as Record. The full layout of each parameter is in abi.json:
jq '.parameters' build/validators/drop/abi.json[ { "name": "admin", "schema": { "encoding": "B", "kind": "bytes" }, "sourceType": "org.julclang.ledger.PubKeyHash" }, { "name": "limits", "schema": { "fields": [ { "name": "maximum", "schema": { "encoding": "I", "kind": "integer" } }, { "name": "minimum", "schema": { "encoding": "I", "kind": "integer" } } ], "identity": "elci.Record:maximum,minimum [Int, Int]", "kind": "record", "tag": 0 }, "sourceType": "Record:maximum,minimum [Int, Int]" }]Note the field order: maximum before minimum, because record fields are encoded sorted by name.
Applying parameters with elci apply
Section titled “Applying parameters with elci apply”elci apply compiles the validator, checks each value and writes the applied script. Pass every parameter with --param name=value, where the value is Plutus Data in cardano-cli’s detailed JSON schema:
elci apply . --validator drop \ --param 'admin={"bytes": "887d8fbb82ce756a9dac9210c3d1235e9d3ff2457a667d311d318142"}' \ --param 'limits={"constructor": 0, "fields": [{"int": 10}, {"int": 1}]}'validator drop: applied 2 parameters; hash 4ba1e0d80a5891c79032b5eccdf0514392f90e90278ce6567a7d3d76; wrote .../token-drop/build/validators/drop/applied.plutus.jsonThe applied artifact has the same format as validator.plutus.json, now with a hash and no remaining parameters:
{ "type": "PlutusScriptV3", "purpose": "minting", "description": "drop", "cborHex": "59010a590107010100333232323223222323232323293370e...", "hash": "4ba1e0d80a5891c79032b5eccdf0514392f90e90278ce6567a7d3d76", "params": "", "sizeBytes": 263}For a minting policy, this hash is the policy id. For a spending validator, the script address is derived from it.
Plutus Data JSON
Section titled “Plutus Data JSON”Each value is one JSON object in the detailed schema:
| JSON | Data | For Elci types |
|---|---|---|
{"int": 42} | integer | Int |
{"bytes": "0a1b"} | byte string, as hex | Bytes, hashes, String (the UTF-8 bytes) |
{"list": [ ... ]} | list | List a |
{"map": [{"k": ..., "v": ...}]} | map | Dict k v (keys in ascending order), Map k v |
{"constructor": 0, "fields": [ ... ]} | constructor | records, custom types, Bool, Maybe, tuples, (), TxOutRef |
Build values with the encoding rules: a record is constructor 0 with its fields sorted by name, True is {"constructor": 1, "fields": []}, and a TxOutRef is {"constructor": 0, "fields": [{"bytes": "<transaction id>"}, {"int": <index>}]}. The parser accepts integers, hex strings, arrays and objects; strings with escape sequences are not supported.
What apply checks
Section titled “What apply checks”Before applying anything, apply runs Julc’s strict decoder for each parameter’s type against the value you gave. Wrong kinds, constructor tags and field counts are rejected, and nothing is written:
elci: ELCI4001: parameter limits is not a well-formed Record:maximum,minimum [Int, Int] value(from {"constructor": 0, "fields": [{"int": 10}]}, which has one field too few), and:
elci: ELCI4001: parameter admin is not a well-formed org.julclang.ledger.PubKeyHash value(from {"int": 42}). A Dict must list its keys in ascending order, each once, so apply also rejects
{"map": [{"k": {"int": 2}, "v": {"int": 10}}, {"k": {"int": 1}, "v": {"int": 5}}]}for a parameter caps : Dict Int Int:
elci: ELCI4001: parameter caps has a Dict whose keys are not strictly ascending; write its keys sorted and without repeatsOther mistakes are reported the same way:
| Mistake | Message |
|---|---|
No --param limits=... | ELCI4001: missing --param limits=<plutus-data-json> |
--param owner=... | ELCI4001: validator drop has no parameter 'owner'; declared: admin, limits |
--param admin=887d8fbb (bare hex, not JSON) | ELCI4001: parameter admin is not Plutus Data JSON (for example {"int": 5}, {"bytes": "ab"} or {"constructor": 0, "fields": []}): unexpected trailing content at offset 3 |
Values from files
Section titled “Values from files”Long values are easier to keep in files. Write @path instead of the JSON. The path is resolved against the project directory, not your current directory:
{ "constructor": 0, "fields": [ { "int": 10 }, { "int": 1 } ]}elci apply . --validator drop \ --param 'admin={"bytes": "887d8fbb82ce756a9dac9210c3d1235e9d3ff2457a667d311d318142"}' \ --param limits=@params/limits.jsonWhere the result goes
Section titled “Where the result goes”By default the result is build/validators/<name>/applied.plutus.json. It belongs to the build: the next elci build of that validator removes it, because it was applied to the previous script. To keep an applied script, write it elsewhere with --output, which is resolved against your current directory:
elci apply token-drop --validator drop \ --param 'admin={"bytes": "887d8fbb82ce756a9dac9210c3d1235e9d3ff2457a667d311d318142"}' \ --param limits=@params/limits.json --output drop-preprod.plutus.jsonvalidator drop: applied 2 parameters; hash 4ba1e0d80a5891c79032b5eccdf0514392f90e90278ce6567a7d3d76; wrote .../drop-preprod.plutus.jsonIf the validator sets expected-abi and the current ABI differs, apply refuses to apply parameters. See Build and deploy.
Applying parameters from Java
Section titled “Applying parameters from Java”Off-chain Java code can apply the same parameters to the unapplied artifact with Julc’s Cardano Client Lib adapter. Pass PlutusData values in the declared order. This example gives the same hash as elci apply above:
import java.math.BigInteger;import java.nio.file.Files;import java.nio.file.Path;import java.util.HexFormat;import java.util.List;import org.julclang.clientlib.JulcScriptAdapter;import org.julclang.clientlib.ValidatorOutput;import org.julclang.core.PlutusData;
var output = ValidatorOutput.fromJson(Files.readString( Path.of("build/validators/drop/validator.plutus.json")));
var admin = PlutusData.bytes(HexFormat.of().parseHex( "887d8fbb82ce756a9dac9210c3d1235e9d3ff2457a667d311d318142"));// Limits { minimum = 1, maximum = 10 }: fields are sorted by name, so maximum comes first.var limits = new PlutusData.ConstrData(BigInteger.ZERO, List.of( PlutusData.integer(BigInteger.TEN), PlutusData.integer(BigInteger.ONE)));
var program = JulcScriptAdapter.toProgram(output.cborHex()).applyParams(admin, limits);var script = JulcScriptAdapter.fromProgram(program); // a Cardano Client Lib PlutusV3ScriptSystem.out.println(HexFormat.of().formatHex(script.getScriptHash()));4ba1e0d80a5891c79032b5eccdf0514392f90e90278ce6567a7d3d76It needs org.julclang:julc-cardano-client-lib (at the Julc version your compiler uses) and Cardano Client Lib on the classpath. Unlike elci apply, this path doesn’t check the values against their types, so validate them yourself.