Skip to content

Helios Haskell Smart Contract Development

Bernard Sibanda edited this page May 7, 2025 · 1 revision

📘 Helios Haskell Smart Contract Development Tutorial

By Bernard Sibanda · 2025-05-07

Coxygen Global

📑 Table of Contents

  1. Prerequisites
  2. Compiling Helios Sources to CBOR
  3. Loading Plutus CBOR in Helios Off-Chain
  4. Parameterized vs. Non-Parameterized Scripts
  5. Constructing Datum & Redeemer
  6. Local CEK Evaluation
  7. Building On-Chain Transactions
  8. Complete Example: Parameterized Vesting
  9. Complete Example: Non-Parameterized “AlwaysSucceeds”
  10. Debugging Tips
  11. Helios Key Classes
  12. CEK Machine & Deserialization Functions
  13. Glossary of Terms

1. 🔧 Prerequisites

  • Helios v0.16.7 loaded as an ES module (helios.js).
  • A Haskell-compiled .plutus validator (JSON-wrapped or raw CBOR).
  • CIP-30 wallet connection (Cip30Wallet, WalletHelper).
  • Familiarity with UTxOs, datums, redeemers, script contexts.

1.1 Purpose of this tutorial

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.


2. 📦 Compiling Helios Sources to CBOR

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 string

3. 🔄 Loading Plutus CBOR in Helios Off-Chain

3.1 JSON-wrapped

import { deserializeUplc } from "helios.js";
const program = deserializeUplc(scriptJson); // { type, version, cborHex }

3.2 Raw CBOR

import { deserializeUplcBytes } from "helios.js";
const bytes   = hexToBytes(rawHex);
const program = deserializeUplcBytes(bytes, { purpose: "spending" });

4. ⚙️ Parameterized vs. Non-Parameterized Scripts

  • Non-param: program.mainFunc.nArgs === 3 (datum, redeemer, ctx).
  • Parametric: nArgs === 4 (params, datum, redeemer, ctx).

4.1 Instantiating Parameters

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 === 3

5. 📋 Constructing Datum & Redeemer

import { 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);

6. 🧪 Local CEK Evaluation

import { NetworkParams } from "helios.js";
const result = await program.runInternal(
  [ datumArg, redeemerArg, ctxArg ],
  null,
  networkParams
);
// returns Boolean

7. 🌐 Building On-Chain Transactions

import {
  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);

8. 🔐 Complete Example: Parameterized Vesting

// 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);

9. ✅ Complete Example: Non-Parameterized “AlwaysSucceeds”

// 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

10. 🔍 Debugging Tips

  • Inspect AST: console.log(program.dumpUPLC())
  • Check arity: program.mainFunc.nArgs
  • Enable traces: config.DEBUG = true
  • Profile: assign program.properties.name to get logs.

11. 💻 Helios Key Classes

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


12. 🧩 CEK Machine & Deserialization Functions

12.1 evalCek — the CEK Evaluator

/**
 * @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");
    }
  }
}

12.2 deserializeUplcBytes — Flat CBOR → UplcProgram

/**
 * 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);
}

12.3 deserializeUplc — JSON Wrapper → UplcProgram

/**
 * 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));
}

13. 📖 Glossary of Terms

  • 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.

Clone this wiki locally