-
Notifications
You must be signed in to change notification settings - Fork 2
Helios Haskell Smart Contract Development
Bernard Sibanda edited this page May 7, 2025
·
1 revision
By Bernard Sibanda · 2025-05-07
- Prerequisites
- Compiling Helios Sources to CBOR
- Loading Plutus CBOR in Helios Off-Chain
- Parameterized vs. Non-Parameterized Scripts
- Constructing Datum & Redeemer
- Local CEK Evaluation
- Building On-Chain Transactions
- Complete Example: Parameterized Vesting
- Complete Example: Non-Parameterized “AlwaysSucceeds”
- Debugging Tips
- Helios Key Classes
- CEK Machine & Deserialization Functions
- Glossary of Terms
-
Helios v0.16.7 loaded as an ES module (
helios.js). - A Haskell-compiled
.plutusvalidator (JSON-wrapped or raw CBOR). - CIP-30 wallet connection (
Cip30Wallet,WalletHelper). - Familiarity with UTxOs, datums, redeemers, script contexts.
This tutorial is help developers understand many few essentials about Helios I found very important. It helps one know these: cbor evaluation steps, CEK evaluation pipeline, classes in helios handling utxos, tx, datums, redeemers and script contexts.
import { Program } from "helios.js";
// 2.1 Non-parameterized
const src1 = `spending
func main(datum: Unit, redeemer: Unit, ctx: ScriptContext) -> Bool { true }
`;
const prg1 = Program.new(src1);
const uplc1 = prg1.compile(); // → UplcProgram AST
const bytes1 = uplc1.serialize(); // → Uint8Array CBOR
// 2.2 Parameterized
const src2 = `spending parametric
struct Params { beneficiary: PubKeyHash, deadline: Int }
func main(p: Params, d: Unit, r: Unit, ctx: ScriptContext) -> Bool {
// logic…
true
}
`;
const prg2 = Program.new(src2);
const uplc2 = prg2.compile();
const hex2 = uplc2.serializeHex(); // → hex stringimport { deserializeUplc } from "helios.js";
const program = deserializeUplc(scriptJson); // { type, version, cborHex }import { deserializeUplcBytes } from "helios.js";
const bytes = hexToBytes(rawHex);
const program = deserializeUplcBytes(bytes, { purpose: "spending" });-
Non-param:
program.mainFunc.nArgs === 3(datum, redeemer, ctx). -
Parametric:
nArgs === 4(params, datum, redeemer, ctx).
import { UplcDataValue, ConstrData, Site } from "helios.js";
// Build Params datum:
const paramsValue = new UplcDataValue(
Site.dummy(),
new ConstrData(0, [ownerPkhData, deadlineData])
);
// Apply:
const instProgram = program.apply([ paramsValue ]);
// now instProgram.mainFunc.nArgs === 3import { ConstrData, IntData, ByteArrayData } from "helios.js";
// Unit
const unit = new ConstrData(0, []);
// Custom datum
const myDatum = new ConstrData(1, [
new IntData(42n),
new ByteArrayData([...])
]);For CEK:
import { UplcDataValue } from "helios.js";
const datumArg = new UplcDataValue(Site.dummy(), myDatum);
const redeemerArg= new UplcDataValue(Site.dummy(), unit);import { NetworkParams } from "helios.js";
const result = await program.runInternal(
[ datumArg, redeemerArg, ctxArg ],
null,
networkParams
);
// returns Booleanimport {
Tx, TxOutput, Datum, Address,
Value, TxInput
} from "helios.js";
// 7.1 Lock ADA
const tx1 = new Tx();
tx1.attachScript(instProgram);
tx1.addInput(myUtxo, unit);
tx1.addOutput(new TxOutput(
Address.fromHashes(instProgram.validatorHash),
new Value(2_000_000n),
Datum.inline(myDatum)
));
tx1.addSigner(myPkh);
await txEnd(tx1);
// 7.2 Claim ADA
const tx2 = new Tx();
tx2.attachScript(instProgram);
tx2.addInput(myLockedUtxo, unit);
tx2.addOutput(new TxOutput(myAddr, new Value(2_000_000n)));
tx2.addSigner(myPkh);
await txEnd(tx2);// Deserialize & instantiate
const raw = /* JSON from Haskell */;
const vestPrg= deserializeUplc(raw);
const params = new UplcDataValue(Site.dummy(),
new ConstrData(0, [ownerPkhData, new IntData(deadlineN)])
);
const inst = vestPrg.apply([params]);
// Lock
const txL = new Tx();
txL.attachScript(inst);
txL.addInput(utxos[0], unit);
txL.addOutput(new TxOutput(
Address.fromHashes(inst.validatorHash),
new Value(2_000_000n),
Datum.inline(paramsDatum)
));
txL.addSigner(myPkh);
await txEnd(txL);
// Claim
const txC = new Tx();
txC.attachScript(inst);
txC.addInput(lockedUtxo, unit);
txC.addOutput(new TxOutput(myAddr, new Value(2_000_000n)));
txC.addSigner(myPkh);
await txEnd(txC);// Compile in Helios
const prg = Program.new(`spending
func main(_,_,_) -> Bool { true }
`);
const uplc = prg.compile();
// Deserialize & test
const testPrg = deserializeUplc({
type: "PlutusScriptType",
version: "1.0.0",
cborHex: uplc.serializeHex()
});
await testPrg.runInternal([ datumArg, redeemerArg, ctxArg ], null, networkParams); // true-
Inspect AST:
console.log(program.dumpUPLC()) -
Check arity:
program.mainFunc.nArgs -
Enable traces:
config.DEBUG = true -
Profile: assign
program.properties.nameto get logs.
class Program
class UplcProgram
class UplcDataValue
class ConstrData
class Datum
class Tx
class TxOutput
class Address
class Value
class TxInput
---
```javascript
// Section 33: Helios root object
export class Program {
#purpose;
#modules;
#config;
#types;
#parameters;
constructor(purpose, modules, config) {
this.#purpose = purpose;
this.#modules = modules;
this.#config = config;
this.#types = {};
this.#parameters = {};
}
static new(mainSrc, moduleSrcs = [], validatorTypes = {}, config = DEFAULT_PROGRAM_CONFIG) {
// parses, type-checks, and returns a Program instance
return Program.newInternal(mainSrc, moduleSrcs, validatorTypes, config);
}
toUplc() {
// emits a UplcProgram
return new UplcProgram(this._ir.expr.toUplc(), this._ir.properties);
}
}// Section 11: Uplc program
export class UplcProgram {
#term; // an UplcTerm AST
#properties; // script version, cost model, etc.
constructor(term, properties) {
this.#term = term;
this.#properties = properties;
}
// runs the CEK evaluator on the term
run(datum, redeemer, context) { … }
calcSize() {
// returns the on-chain size of the compiled script
return this.#term.calcSize();
}
}// Section 10: Uplc AST values
export class UplcDataValue extends UplcValueImpl {
#data; // a UplcData AST node
constructor(site, data) {
super(site);
this.#data = data;
}
// used by the evaluator to inject on-chain datum/redeemer into the runtime
transfer(other) {
return other.transferUplcDataValue(
this.site.transfer(other),
this.#data.transfer(other)
);
}
}// Section 6: Uplc data types
export class ConstrData extends UplcData {
#index;
#fields; // array of UplcData
constructor(index, fields) {
super();
this.#index = index;
this.#fields = fields;
}
toCbor() {
// CBOR-encodes the constructor + its fields
return Cbor.encodeConstr(this.#index, this.#fields.map(f => f.toCbor()));
}
}// Section 35: Tx types
export class Datum extends CborData {
// user-facing wrapper around either inline or hashed datum
static fromCbor(bytes) { … }
static fromUplcData(data) { … }
}(plus its two subclasses HashedDatum and InlineDatum)
export class Tx {
#body;
#witnesses;
#scripts;
constructor() {
// builds an empty transaction
this.#body = new TxBody();
this.#witnesses = new TxWitnesses();
this.#scripts = [];
}
addInput(utxo, redeemer) { … }
addOutput(txOutput) { … }
attachScript(uplcProgram) { … }
addSigner(pubKeyHash) { … }
finalize() { … } // balances fee, auto-sets validity range, runs CEK to validate
}export class TxOutput {
constructor(address, value, datum = null) {
this.address = address;
this.value = value; // a helios Value
this.datum = datum; // either InlineDatum or HashedDatum
}
toCbor() { … }
}export class Address {
static fromHashes(validatorHash, stakeHash = null) { … }
toBech32() { … }
}export class Value {
constructor(coin, assets = {}) {
this.coin = coin; // bigint lovelace
this.assets = assets; // Map<PolicyId, Map<AssetName, bigint>>
}
add(value) { … }
toCbor() { … }
}export class TxInput {
constructor(txId, utxoIdx) {
this.txId = txId; // TxId
this.utxoIdx = utxoIdx; // index within the transaction
}
toCbor() { … }
toOutputIdData() { … }
}With these class signatures in hand you can show code snippets in your tutorial like:
// build a parameterized vesting script off-chain
const program = Program.new(heliosSource);
const uplc = program.toUplc();
// lock UTxO with datum
const datum = new ConstrData(0, [ownerPkh, closureDeadline]);
const txOut = new TxOutput(scriptAddr, minAdaValue, Datum.inline(datum));
tx.addOutput(txOut);
// later, spend with empty redeemer
const redeemer = new ConstrData(0, []);
tx.addInput(scriptUtxo, redeemer);
// finalize runs the CEK evaluator under the hood
await tx.finalize();CEK Functions & Deserialization
/**
* @internal
* @param {UplcRte} rte
* @param {UplcTerm} start // initial UPLC AST (with any args wrapped)
* @param {null | UplcValue[]} args
* @returns {Promise<UplcValue>}
*/
export async function evalCek(rte, start, args = null) {
// 1) Wrap arguments in UplcCall or UplcForce
if (args !== null) {
if (args.length === 0) {
start = new UplcForce(start.site, start);
} else {
for (let arg of args) {
start = new UplcCall(start.site, start, new UplcConst(arg));
}
}
}
// 2) Initial CEK state
const stack = [];
let state = { computing: start, env: { values: [], callSites: [] } };
rte.incrStartupCost(); // account for startup
// 3) Main loop
while (true) {
if ("computing" in state) {
// Evaluate a term → new state
state = state.computing.computeCek(rte, stack, state);
} else if ("reducing" in state) {
// Pop a frame or finish
const frame = stack.pop();
const term = state.reducing;
if (!frame) {
if (term instanceof UplcConst) {
return term.value; // final constant
} else {
throw new Error(
"final UplcTerm in CEK isn't a UplcConst but a " + term.toString()
);
}
}
state = await frame.reduceCek(rte, stack, state);
} else if ("error" in state) {
// Render errors + trace
throw state.error;
} else {
throw new Error("unhandled CEK state");
}
}
}/**
* Deserializes a flat-encoded UPLC program.
* @param {number[]} bytes // CBOR bytes array
* @param {ProgramProperties} properties
* @returns {UplcProgram}
*/
export function deserializeUplcBytes(
bytes,
properties = { purpose: null, callsTxTimeRange: false }
) {
return UplcProgram.fromFlat(bytes, properties);
}/**
* Parses a JSON-wrapped Plutus Core script:
* { cborHex: "..." }
* @param {string | {cborHex: string}} json
* @returns {UplcProgram}
*/
export function deserializeUplc(json) {
const obj = typeof json === "string" ? JSON.parse(json) : json;
if (!("cborHex" in obj)) {
throw UserError.syntaxError(
new Source(
typeof json === "string" ? json : JSON.stringify(json, null, 2),
"<json>"
),
0,
1,
"cborHex field not in JSON"
);
}
// Decode hex to bytes and hand off to fromCbor (handles version header)
return UplcProgram.fromCbor(hexToBytes(obj.cborHex));
}- CBOR: Concise Binary Object Representation; binary encoding for Plutus Core.
- Helios: A TypeScript library & DSL compiling to Plutus Core.
- UplcProgram: In-memory AST of a Plutus Core script.
- ConstrData: Constructor wrapper for on-chain data (e.g. enums, unit).
-
UplcDataValue: A CEK-ready wrapper carrying
UplcData. - CEK Machine: Call-by-value evaluator for UPLC in Helios.
- Datum: On-chain data attached to a UTxO.
- Redeemer: Off-chain argument provided when spending a script UTxO.
- ScriptContext: Transaction context (inputs, outputs, time range, signers).
- Parametric Script: A validator with extra lambda parameters baked in.
-
serialize() / serializeHex(): Methods to get CBOR bytes or hex from
UplcProgram.