Introduction to Helios

Helios is a DSL (Domain Specific Language) for writing smart contracts for the Cardano blockchain.

The Helios library is written in Javascript and has an API for:

  • compiling Helios sources into Plutus-Core
  • building and submitting transactions to the Cardano blockchain

Code sample

// all Helios programs begin with a script purpose
spending always_true 

// the 'main' function contains the core validator logic
// returns true if a given UTxO is allowed to be spent
func main(_, _, _) -> Bool {
    // Helios is an expression based language
    true
}

// Note: the datum, redeemer, and scriptcontext are ignored by the underscores

Structure of this book

Before starting to use Helios to create smart contracts and build DApps it is important to understand Cardano's eUTxO model very well. If you don't yet, we recommend you read the Understanding eUTxOs preface first.

Chapter 1 covers the language itself, including a complete reference of the Helios builtins.

Chapter 2 covers setting up the Helios library, compiling smart contracts, and building and submitting smart contract transactions, using only Javascript.

Chapter 3 covers how to use Helios smart contracts with cardano-cli.

Chapter 4 covers minting policies, exploits to be aware of, some more complex scripts, and some recommendations for building DApps.

Understanding the eUTxO model

Before we get into any coding we first need to understand how smart contracts work on Cardano and how Cardano differs from the more conventional account-based model.

Note: eUTxO is an abbreviation of extended Unspent Transaction Outputs

Account-based model vs eUTxO model

Smart contracts on Cardano are quite different from those on Ethereum.

Ethereum-style smart contracts (account-based)

When a transaction occurs on an account-based blockchain, the balance of the sender's account is directly decremented and that of the recipient is incremented, similar to how conventional bank accounts work.

Contracts interact with these balances and run via the EVM (Ethereum Virtual Machine). The EVM can be thought of as a global on-chain computer on which smart contracts take turns running, before their results are added to the chain.

Note: the data of all accounts on Ethereum are stored in a Merkle-Patricia trie, which is like a fancy hashmap. After all the transactions in a block are run, the root hash of the block trie is added to the chain.

The eUTxO model

In the eUTxO model tokens are stored in UTxOs. A UTxO is like (electronic)-cash where each individual bundle of bills (Ada and native-tokens) is stored separately.

A transaction in the UTxO model takes one or more UTxOs as transaction inputs, which are destroyed, and creates one or more UTxOs as transaction outputs.

Transactions in an account-based model mutate the data-points storing the total balances. This is very risky (regular banks are insured against this, and also have paper backups in case of mistakes, but blockchains have no such fallbacks). In the UTxO model only the "bills" that participate in a given transaction can potentially be affected (which is bad, but not catastrophic).

Of course a UTxO chain can emulate account-based chains (by storing all tokens in a single UTxO) and account-based chains can emulate UTxO chains (by spreading a user's balance over many different accounts).

Components of a UTxO on Cardano

UTxOs have 3 main components:

  • an address
  • tokens (Ada and other native assets)
  • a datum

Address

The address of a UTxO determines the owner (i.e. who has the right to spend it).

A user's balance is calculated by summing all UTxOs sitting at addresses owned by that user.

An address can either be derived from the hash of a user's public key (PubKeyHash in Helios), or the hash of a validator script (ValidatorHash in Helios). In the latter case the script effectively becomes the owner of the UTxO.

Tokens

Each UTxO contains some tokens, which represent value on the blockchain. Tokens have positive value due to scarcity (tokens can't be duplicated) and utility (eg. Ada being used to pay transaction fees).

Datum

The datum is data that is attached to UTxOs. A datum represents the state of a smart contract, and is immutable (smart contract state can change though, by spending old UTxOs and creating new ones).

The 'e' (extended) in eUTxO comes from the datum. The Bitcoin UTxO model doesn't have datums, giving Bitcoin scripts very limited capabilities. Extending the UTxO model, as done by Cardano and Ergo, gives an eUTxO model the same capabilities as an account-based model, while benefitting from a much safer approach to transactions (because a global state isn't being accessed/mutated).

Validator scripts

A validator script is a function that is evaluated when a transaction attempts to spend a UTxO locked at that script's address. This function takes 3 arguments:

  • the datum attached to the UTxO
  • some data provided by the user who created the transaction (the redeemer)
  • details about the transaction (the script context)

The validator script then calculates whether or not the UTxO is allowed to be spent (essentially returning a boolean result).

Separating the validation logic from the transaction building/submitting logic makes it much easier to audit the trusted part of eUTxO DApps.

Helios is all about writing these validator scripts.

Note: a UTxO can only be spent once. In every transaction all input UTxOs are destroyed, and new output UTxOs are created.

Note: a valid transaction must satisfy the following conditions:

  • the transaction must be balanced: the total amount of tokens in the transaction inputs must be equal to those in the transaction outputs (minus the fees, plus the minted tokens).
  • the validators for all the transaction inputs must evaluate to true.

Pros and cons of the eUTxO Model

Pros

  • Deterministic transaction fees

    eUTxO smart contract evaluation is deterministic. This means that you can calculate the resource usage of a transaction before posting it to the blockchain. The transaction fees for a transaction can thus be calculated deterministically off-chain.

    The transaction fees of account-based blockchains depend on network load, and can vary a lot.

  • Transaction fees not charged upon failure

    The determinism of the eUTxO model means that transaction success can be determined before posting. Transaction failure is still possible due to contention, but this is a very cheap check, and no fee is charged.

    Transaction failure on account-based blockchains results in losing the fee, as significant processing power might've been used before encountering the failure condition.

  • Easier to audit

    Auditing of eUTxO smart contracts is much easier because only the validation function needs to be audited, which has a very locally-scoped nature.

  • Concurrency

    Due to monetary value being naturally spread over many UTxOs, a UTxO blockchain can be compared to an extremely sharded account-based blockchain (some smart contracts might require a centralized data-point though, and won't allow concurrent interactions, see contention).

  • Better for layer 2

    The local nature of UTxOs allows reusing validation logic in layer 2 scaling solutions such as state channels (see hydra).

  • Simpler

    Though not immediately obvious, eUTxO smart contracts are often much simpler than an equivalent Solidity smart contract (this will become apparant when you start to use Helios).

Cons

  • Contention

    If eUTxO contracts aren't designed properly they can encounter contention problems. Contention occurs when two or more transactions try to spend the same UTxO. If this happens only one of the transactions will succeed, and the others will fail (resulting in an unpleasant user experience).

    This is usually not an issue on Ethereum because the EVM handles ordering smart contract calls.

    Note: there are ways to avoid contention, by for example taking advantage of the parallel nature of UTxOs (see SundaeSwap's scooper model)

Further reading

If you feel like you still don't fully understand the eUTxO model, we recommend you keep reading:

Changelog

This page documents breaking changes and major features of major version releases

v0.13

Language

API

  • helios.Int renamed to helios.HInt
  • helios.HeliosString renamed to helios.HString
  • helios.HeliosMap renamed to helios.HMap
  • helios.List renamed to helios.HList
  • program.changeParam() deprecated

Helios language reference

The Helios language is a purely functional programming language, with a simple curly braces syntax. It is inspired by Go and Rust.

Tenets

  • Helios should be readable by as many programmers as possible, readability is more important than writeability
  • Helios is opionated, there should be only one obvious way of doing things
  • Helios is safe, it should be easy to spot malicious code

Structure of this chapter

This chapter starts by explaining the basic syntax of Helios, and then moves onto higher level concepts like functions, structs and enums.

This chapter ends with a complete reference of the Helios builtins.

Comments

Helios comments are C-like.

Single-line comments

Single-line comments use two forward slashes (//):

func main(_, _, _) -> Bool {
	// This is a comment.
	true
}

Multi-line comments

Multi-line comments use /* ... */:

func main(_, _, _) -> Bool {
	/*
		This is a multi-line comment.
	*/
	true
}

Variables

Helios doesn't really have variables as it is a purely functional programming language, and nothing can be mutated after definition. It is more accurate to think of variables in Helios as binding values to names.

Assignment

Inside a function body, values can be bound to names using assignment expressions:

my_number: Int = 42; ...

Here my_number has value 42, and has type Int. my_number cannot be mutated after its definition.

Assignment expressions must be followed by another expression, separated by a semicolon (;). The assignment expression above should be seen as syntactic sugar for the following anonymous function call: ((my_number: Int) -> {...})(42).

Note: Int is Helios' only number type, and represents an unbounded integer.

Note: an assignment expression can alternatively be seen as a ternary operator: ... = ... ; ...

const statements

Values can also be bound to names at the top-level of a script, or inside struct or enum blocks. This is done with the const keyword:

const AGE: Int = 123

Top-level const statements can be re-bound using the Helios API to form a new, distinct contract with a different address (see parameterized contracts).

Note: the right-hand side of const can contain complex expressions and even function calls. The compiler is smart enough to evaluate these at compile-time.

Type annotations

Assignment expressions and const statements usually include a type annotation. For literal right-hand sides type annotations are optional:

list_of_ints = []Int{1, 1, 2, 3, 5}; ...

// instead of the more verbose:

list_of_ints: []Int = []Int{1, 1, 2, 3, 5}; ...

Primitive types

Helios has 4 primitive types:

  • Int (an unbounded integer)
  • Bool (true or false)
  • ByteArray (array of uint8)
  • String (utf-8 text)

Int

Helios' Int type represents an unbounded integer (like Haskell's Integer type).

// Helios supports typical integer literals:
my_decimal = 17;
my_binary  = 0b10001;
my_hex     = 0x11;
my_octal   = 0o121; ...

Int is the only numeric type in Helios. There is no float type.

More information about the Int type can be found here.

Bool

The Bool type has two possible literal values: true or false.

Booleans are used throughout validator scripts, and the return type of validator scripts is a boolean. The simplest validator script body is just a literal boolean:

func main(_, _, _) -> Bool {
    true
}

The == and != operators, returning boolean results, are defined on all builtin and user types:

func main(_, _, ctx: ScriptContext) -> Bool {
    ctx == ctx // always true
}

More information about the Bool type can be found here.

ByteArray

The ByteArray type, as you've likely guessed, represents an array of bytes. A literal ByteArray is a hexadecimal sequence prefixed by #:

my_bytes = #af2e221a; ... // 

All builtin and user types can be converted into a ByteArray using the builtin serialize method:

cbor_bytes: ByteArray = 123.serialize(); ... // cbor encoding of 123

More information about the ByteArray type can be found here.

Note: a ByteArray can be empty, so the following is valid syntax: my_bytes = #; ...

String

A literal Helios string uses double quotes ("..."):

my_message = "hello world"; ...

Similar to all other values in Helios, strings are immutable and have a fixed length.

Strings cannot grow after definition. Concatenating two strings creates a new string:

string_1: String = "Hel";
string_2: String = "ios";
result: String = string_1 + string_2; ... // "Helios"

More informationa about the String type can be found here.

Container types

Helios has 3 container types:

  • List (linked list)
  • Map (association list of key-value pairs)
  • Option (equivalent to Maybe in Haskell)

List

Helios has a builtin linked list type, similar to Haskell's List. The syntax for a list type is []ItemType where ItemType is a parameter type that represents the type of the contained items. The item type can be any type except a function type.

List literals have a syntax similar to Go:

my_ints = []Int{1, 2, 3, 4, 5};

x: Int = some_ints.get(2); ...   // x == 3

Note: lists aren't indexed with [...]. Instead the get method can be used. Indices are 0-based.

More information about lists can be found here.

Map

A Map in Helios is internally represented as a list of key-value pairs. Both key and value can have any type except a function type. Uniqueness of keys isn't guaranteed.

A Map has a type syntax and a literal syntax similar to Go:

// either side of the colon can be an arbitrary expression 
//  that evaluates to the correct type
my_map = Map[String]Int{
    "zero": 0,
    "one":  1,
    "two":  2
}; ... 

print(my_map.get("zero").show()); ... // prints '0'

More information about maps can be found here.

Option

The Option type is a builtin enum with type syntax Option[SomeType]. It is internally defined as:

enum Option[SomeType] {
    Some { some: SomeType }
    None
}

An Option is instantiated like any other enum:

some_int = Option[Int]::Some{42};

none_int = Option[Int]::None; ...

More information about Option can be found here.

Branching

Helios has conventional if-else syntax for branching:

if (tag == 0) {
    23
} else if (tag == 1) {
    42
} else {
	0
}

The last expression in each branch block is implicitly returned (much like Rust).

The following is valid syntax:

x: Int = 
	if (true) {
		42
	} else {
		24
	}; ...

print, error, assert

There are three builtin void functions. These can be used to create user-defined void functions.

Void functions can't be used in assignments.

print

For debugging purposes, Helios has a special print expression. print(...) takes a String argument:

func main() -> Bool {
	print("Hello world");
	true
}

Note: print expressions are useful when debugging scripts. They are however eliminated by the compiler when compiling scripts optimized for production.

error

Helios has a special error builtin, which can be used to throw errors inside branches of if-else expressions, and cases of switch expressions. At least one branch or case must be non-error-throwing for the if-else or switch expression to return a non-void value.

if (cond) {
    true
} else {
    error("my error message")
}
x.switch{
    Buy => true,
    Sell => msg = "my error message"; error(msg)
}

assert

The builtin assert function throws an error if a given expression evaluates to false.

assert(condition, "should be true"); ...

Functions

Functions are a core feature of Helios. All Helios functions are pure, which means they don't have side effects and always return the same result when given the same arguments.

Function statements are defined using the func keyword. Helios has no return statement, the last expression in a function is implicitly returned (like in Rust):

func add(a: Int, b: Int) -> Int {
    a + b 
}

A function can call itself recursively:

func fib(n: Int) -> Int {
    // the branches of an if/else expresion return values
    if (n < 1) {
        1
    } else {
        fib(n - 1) + fib(n - 2)
    }
}

Note:: a function can only reference itself when recursing. Helios doesn't support hoisting, so mutual recursion by referring to functions defined after the current function isn't possible:

01 func a(n: Int) -> Int {
02     b(n)                   // ReferenceError: 'b' undefined
03 }
04
05 func b(n: Int) -> Int {
06     a(n)                   // ok
07 }

More advanced information can be found on the following pages:

Multiple return values

A Helios function can return multiple values:

func swap(a: Int, b: Int) -> (Int, Int) {
    (b, a)
}

You can assign to multiple return values:

(a: Int, b: Int) = swap(10, 20); ... // a==20 && b==10

Some of the multiple return values can be ignored with an underscore (_):

(a: Int, _) = swap(10, 20); ...

Void functions

Functions that are composed of only print, error, assert, and if-else/switch expressions there-of, return void (()). These kinds of functions can't be called in assignments.

func assert_even(n: Int) -> () {
    assert(n % 2 == 0, "not even")
}

The syntax for calling user-defined void functions is the same as for print, error and assert:

func main(ctx: ScriptContext) -> Bool {
    assert_even(ctx.outputs.length);
    ...
}

Anonymous functions

Helios also supports anonymous function expressions. Anonymous function expressions are basically function statements without the func keyword:

// type of 'is_even' can be inferred
is_even = (n: Int) -> Bool { n % 2 == 0 }; ...

The return type of anonymous functions is optional:

is_even = (n: Int) -> { n % 2 == 0 }; ...

Note: function statements can be referenced by their name, returning a function value. This should be preferred to using anonymous functions, as it is more readable.

Unused arguments

All named function arguments must be used in the function definition. This can be inconvenient when defining callbacks where you want to ignore some of the arguments. For this situation you can use an underscore (_):

// sort a map by only comparing the keys
map.sort((a_key: ByteArray, _, b_key: ByteArray, _) -> Bool {
    a_key < b_key
})

Underscores are most commonly used to ignore either the datum, redeemer, or the ScriptContext, in the main function of a validator script.

Optional arguments

Some function arguments can be made optional by specifying default values:

func incr(x: Int, by: Int = 1) -> Int {
    x + by
}

Optional arguments must come last.

The type signature of a function with optional arguments differs from a regular function:

fn: (Int, ?Int) -> Int = incr

Named arguments

Similar to literal constructor fields, function arguments can be named in a call:

func sub(a: Int, b: Int) -> Int {
    a - b
}

sub(b: 1, a: 2) // == 1

Named arguments can't be mixed with positional arguments.

Named arguments are mostly used when calling the copy() method.

Function values

Functions are first-class citizens in Helios and can be used as values. This means:

1. Functions can be passed as arguments

even_numbers: []Int = ([]Int{1, 2, 3, 4, 5, 6}).filter(is_even); ... // [2, 4, 6]; 

2. Functions can be returned

add_a = (a: Int) -> (Int) -> Int { (b: Int) -> {a + b} }; ...

Note: functions aren't entirely first-class to be precise. Functions can't be stored in containers, nor in structs/enums.

The following is a more involved example of a function in Helios.

Example: Collatz sequence

One of my favorite things in maths is the Collatz sequence. A Collatz sequence starts with a given number, n, and calculates the next number in the sequence using the following rules:

  1. if n == 1 the sequence ends
  2. if n is even the next number is n / 2
  3. if n is odd the next number is (n * 3) + 1

Curiously the Collatz sequence always converges to 1, regardless the starting number.

The following function generates the Collatz sequence as a (reversed) list of integers:

// eg. collatz(10, []Int{}) == []Int{10, 5, 16, 8, 4, 2, 1}
func collatz(n: Int, sequence: []Int) -> []Int {
	updated_sequence: []Int = sequence.prepend(n);

    // Rule (1)
    if (n == 1) {
		updated_sequence

    // Rule (2)
    } else if (n % 2 == 0) {
        collatz(n / 2, updated_sequence)

    // Rule (3)
    } else {
        collatz(n * 3 + 1, updated_sequence)
    }
}

Operators

The following operators are defined on many of the builtins:

OperatorPrecedence
-(unary)7
+(unary)7
!(unary)7
%6
/6
*6
- (binary)5
+ (binary)5
>=4
>4
<=4
<4
!=3
==3
&&2
||1

Note: all unary operators have right-to-left associativity. All binary operators have left-to-right associativity.

Note: == and != do a deep comparison and are defined automatically on all user-defined and builtin types.

Structs

A struct in Helios is a named grouping of types (sometimes called a product type). They are similar to structs in other languages (e.g. C, Go and Rust):

// example of a Rational (fractional type)
struct Rational {
    top:    Int
    bottom: Int
}

Note: a struct can't be empty must have at least one field.

Instantiating a struct

A struct can be instantiated using the following literal syntax:

const x = Rational { 1, 3 } // type of 'x' is inferred

The fields can also be named:

const x = Rational { bottom: 3, top: 1 }

Enums

Enums are used to represent types that have multiple variants (sometimes called tagged unions or sum types). These are useful for datums and redeemers.

Example of an enum:

enum Redeemer {
	Cancel
	Buy { buyer: PubKeyHash }
}

// instantiating an enum:
const my_redeemer = Redeemer::Buy { PubKeyHash::new(#...) } 
// type of 'my_redeemer' is inferred

Note: the OOP analogy of an enum is an abstract class, and the enum variants can be thought of as concrete implementations (i.e. child classes).

Note: enum variants without fields don't use braces.

switch

A switch expression is used to perform different actions depending on the enum variant. It is similar to a switch statement in C or Go (and dissimilar to a match expression in Rust, as Helios doesn't support pattern-matching):

enum Datum {
	// each variant has a syntax similar to a struct
    Submission{...} 
    Queue{...}
    Post{...}
}

func main(datum: Datum) -> Bool {
	datum.switch {
		x: Submission => { 
			... // expression must use x
		},
		Queue => {
			... // x not used, so can't be declared
		},
		else => true // default must come last if all sub-types of Datum aren't handled explicitely
		// braces surrounding the cases are optional
	}
}

Data

Data can be thought of as a special builtin enum with 5 members:

A switch expression over Data can use any of these as case types:

data.switch{
	i: Int => ...,
	b: ByteArray => ...,
	l: []Data => ...,
	m: Map[Data]Data => ...,
	e: MyEnum => ... 
}

or

data.switch{
	i: Int => ...,
	b: ByteArray => ...,
	l: []Data => ...,
	m: Map[Data]Data => ...,
	(index: Int, fields: []Data) => ... 
}

Note: the default else case can also be used as a substitute for any of these cases.

Note: besides the builtin types only one enum type can be used in a Data switch, and structs/enum-members can't be used. If an enum is used then (Int, []Data) can't be used.

Methods

You can define methods for structs and enums. The syntax for this is similar to many OOP languages: methods are defined by placing func statements inside a struct or enum block:

struct Rational {
    top:    Int
    bottom: Int

    // 'self' implicitely has type 'Rational'
    func add(self, rhs: Rational) -> Rational {
        top:    Int = (self.top * rhs.bottom) + (rhs.top * self.bottom);
        bottom: Int = self.bottom * rhs.bottom;

        Rational { top, bottom }
    }
}

const example_rational: Rational = Rational { 7, 21 }

const result: Rational = example_rational.add(example_rational)

Methods are accessed using a . (i.e. a dot). Methods cannot modify self as all Helios values are immutable (instead they should return new instantations of the own type).

Note: self is a reserved word and can only be used for the first argument of a method. The self argument can't have a type annotation and is always implicitely typed.

Methods can be used as values

A method is syntactic sugar for a curried function (a function that returns a function) that takes self as it's first argument:

// the following:
rational_1.add(rational_2); ...
// desugars into: __user__Rational_add(rational_1)(rational_2)
//  of type (Rational) -> (Rational) -> Rational

A method value is a function, and can be seen as a closure over self:

// 'rational_1.add' returns a function of type ((Rational) -> Rational) 
//   which can be used just like any other function value
add_to_rational_1: (Rational) -> Rational = rational_1.add; ...

// Note: add_to_rational_1(rational_2) == rational_1.add(rational_2)

Associated functions and constants

Associated functions (aka static methods) and constants are just like regular functions or constants but are also namespaced by a type, for example Rational::new(top, bottom).

Defining associated functions and constants

Associated functions are defined just like methods but without the self argument. Associated constants are simply const statements inside a struct or enum block:

struct Rational {
    top:    Int
    bottom: Int

	// associated const
	const PI = Rational { 355, 113 }

	// associated method
	func new(top: Int, bottom: Int) -> Rational {
		Rational { top, bottom }
	}
}

Using associated functions and constants

Associated functions and constants are namespaced by the type they are associated with and can be referenced using a double colon (::) just like in Rust. For example:

half: Rational = Rational::new(1, 2); ...

Automatic methods

The following (associated) methods and operators are automatically defined on all user and builtin types.

==, !=

The equality and inequality operators are automatically defined on every user-type.

copy

Instantiates a copy of the underlying value, with some of the fields changed.

This method has the same number of arguments as the number of fields in the user-defined struct or enum-variant of which it is a member. Each argument of copy has the same name as the corresponding field and is optional.

struct Pair {
    first:  Int
    second: Int
}

...

pair = Pair{1, 2};

pair.copy(second: 3) // == Pair{1, 3}

from_data

from_data is an associated method that is automatically defined on every user-type, and thus from_data is a reserved name that can't be used for other methods.

from_data converts a typeless Data into something typed.

MyType::from_data(data: Data) -> MyType

serialize

The serialize method is automatically defined on every user-type, and thus serialize is a reserved name that can't be used for other methods.

serialize serializes the underlying data using cbor encoding.

my_instance.serialize() -> ByteArray

Note: when debugging you can inspect the output of print(my_data.serialize().show()) using this cbor tool.

The following is a complete example of a struct with both associated and regular methods.

Example: Rational

struct Rational {
    top:    Int
    bottom: Int

    // associated const
    const PI = Rational{ 355, 113 }

    // associated method
    func new(top: Int, bottom: Int) -> Rational {
        Rational { top, bottom }
    }

    // regular method
    func add(self, rhs: Rational) -> Rational {
        top:    Int = (self.top * rhs.bottom) + (rhs.top * self.bottom);
        bottom: Int = self.bottom * rhs.bottom;

        Rational { top, bottom }
    }

}

const rational_1: Rational = Rational::PI // 355/113 or 3.14159...
const rational_2: Rational = Rational::new(1, 2) // 1/2 or 0.5
const rational_3: Rational = rational_1.add(rational_2) // 823/226 or 3.64159...

Structure of a script

Helios validator scripts have a function called main that returns a boolean (true or false) when validating the spending of a UTxO.

For a spending validator, main takes three arguments:

  • Datum: data stored on-chain that is linked to the locked UTxO (not avaiable for minting/staking scripts)
  • Redeemer: data specified by the user attempting to spend the locked UTxO
  • ScriptContext: information about the transaction spending the locked UTxO

Datum and Redeemer are user-defined but ScriptContext is a builtin type.

The structure of a validator script looks as follows:

// --- (1) ---
spending my_validator       

// --- (2) ---
struct Datum {..}           

// --- (3) ---
enum Redeemer {..}          
                            
// --- (4) ---
func main(datum: Datum, redeemer: Redeemer, ctx: ScriptContext) -> Bool {
    ...                  
}

// --- (5) ---
const MY_DATUM = Datum {...}

Script purpose (1)

In Helios all scripts start with a script purpose, followed by the name of the script. There are four script purposes currently:

  • spending
  • minting
  • staking
  • testing
  • module

On this page we are only concerned with the spending script purpose:

spending my_validator

...

module is covered in the next section.

minting, staking and testing will be covered in the advanced concepts chapter.

Note: the name of each Helios source is registered in the global scope, so these names can't be used by statements, nor for the lhs of assignments. So eg. the entrypoint script can't be named main as that would conflict with the entrypoint function.

Datum (2)

Each UTxO locked at a script address will also have an associated datum. The script can choose to use the datum as part of the spending validation, or it can choose to ignore the datum if it is irrelevant.

If the script uses the datum then a struct or enum must be defined above main that is named Datum.

Redeemer (3)

Each UTxO used as an input for a transaction also has a redeemer attached. This is data specified by the user attempting to spend that UTxO. The script can again choose to use or ignore the redeemer during validation.

If the script uses the redeemer then a struct or enum must be defined above main that is named Redeemer.

main function (4)

The main function (4) of a validator script accepts up to three optional arguments and returns a Bool:

  • datum (2)
  • redeemer (3)
  • script context

Each main argument is optional, but must appear in that order.

spending my_validator

...

func main(datum: Datum, redeemer: Redeemer, context: ScriptContext) -> Bool {
    ...
}

...

Most of the data needed for writing useful validators is contained in the ScriptContext.

Note: The datum and the redeemer are user-defined types (structs or enums) that currently must be named Datum and Redeemer.

Data generators and test functions (5)

After the main function you can define functions and constants for:

  • generating data structures (eg. datums or redeemers)
  • testing the main function

The API has special functionality for working with these:

  • program.parameters an object that evaluates/sets any top-level constant in a Helios source

Some compiler restrictions are lifted in this part of the script:

  • not all names need to be used (relevant for function arguments and assignments)
  • structs can be empty

Special constructors, that aren't available in main, become available in this part of the script:

The following example is the most trivial possible script.

Example: always_succeeds

This basic script allows locked UTxOs to be spent any way the user wants:

spending always_succeeds

func main(_, _, _) -> Bool {
    true
}

You must use an underscore (_) for unused arguments. In this case all three arguments of main are ignored by using an underscore.

Parameterized Contracts

Parameterizing contracts allows dApp developers to create separate instances of a Helios program.

In Helios, this is done by re-binding one or more top-level const PARAM_NAME [...] declarations.

After re-binding any const parameters to a different value, the resulting program will always have a different contract address.

Example

In this example OWNER is a parameter.

spending my_validator

const OWNER = PubKeyHash::new(#)

func main(_, _, ctx: ScriptContext) -> Bool {
    ctx.tx.is_signed_by(OWNER)
}

The parameter can be changed before compiling to the final Uplc format:

const program = helios.Program.new(src);

program.parameters.OWNER = new helios.PubKeyHash("...");

const uplcProgram = program.compile();

Many Helios API types can be used when rebinding the parameters. Also the user-defined types are available through program.types. Besides using Helios API types, Javascript primitive objects (i.e. JSON-like) can be used to rebind a parameter in some cases.

Contrast with Datum

Attaching Datum data structures to specific UTxO's is another way that a validator or other program can have varying behavior.

Using Datum causes those explicit details to be included in UTxO's (and/or in transactions consuming them). Transactions spending the UTxO's held at the same script address can each access and use those various Datum details. Noteably, any interested party can trivially query to discover all the various UTxO's held at a single contract address.

By contrast, two different instances of a parameterized contract, otherwise identical, will have separate addresses where UTxO's can be held. UTxO's don't need to explicitly contain the parameter values.

Querying for UTxO's in separate instances of a parameterized contract is also possible, but requires the interested party to have sufficient knowledge of those separate instance addresses, or other publicly-visible attributes of the target transactions.

Note that any parameterized contract can also use per-UTxO Datum values, as needed.

Modules

Helios top-level statements can be placed in modules and can then be imported by other Helios sources. Modules can be made available during compile-time by including them in a list as the second argument of the Program constructor.

import

import statements in Helios are similar to Javascript:

import { 
    ImportedName1 as NewName1,
    ImportedName2
} from my_module

The imported names act as if the original statements were defined in the source where they are imported.

Note: currently every top-level statement is public and exported by default, including other import statements.

Builtins

This section contains a reference of all the Helios builtins.

Primitive types

Container types

Money types

Time types

  • Duration (i.e. the difference of two Time instances)
  • Time
  • TimeRange (i.e. a period bound by two Time instances)

Hash and cryptography types

Transaction types

Address

Represents a Cardano address.

Associated functions

new

Construct a new Address from a Credential and an optional StakingCredential:

Address::new(
    credential: Credential, 
    staking_credential: Option[StakingCredential]
) -> Address

from_data

Address::from_data(data: Data) -> Address

Getters

credential

Get the payment Credential of an Address:

address.credential -> Credential

staking_credential

Get the StakingCredential of an Address:

address.staking_credential -> Option[StakingCredential]

Operators

==

Address == Address -> Bool

!=

Address != Address -> Bool

Methods

serialize

address.serialize() -> ByteArray

AssetClass

Represents a unique token on the blockchain using its MintingPolicyHash and its token name (as a ByteArray).

Associated functions and constants

ADA

Lovelace AssetClass (empty MintingPolicyHash and empty token name ByteArray):

AssetClass::ADA -> AssetClass

new

Constructs a new AssetClass using a MintingPolicyHash and a token name ByteArray:

AssetClass::new(
    policy_hash: MintingPolicyHash, 
    token_name: ByteArray
) -> AssetClass

from_data

AssetClass::from_data(data: Data) -> AssetClass

Operators

==

AssetClass == AssetClass -> Bool

!=

AssetClass != AssetClass -> Bool

Methods

serialize

asset_class.serialize() -> ByteArray

Bool

Represents a boolean value (true/false).

bool_true  = true;
bool_false = false; ...

Associated functions

and

Doesn't evaluate the second argument if the first argument evaluates to false.

Bool::and(fn_a: () -> Bool, fn_b: () -> Bool) -> Bool

or

Doesn't evaluate the second argument if the first argument evaluates to true.

Bool::or(fn_a: () -> Bool, fn_b: () -> Bool) -> Bool

from_data

Bool::from_data(data: Data) -> Bool

Operators

!

Boolean not operator.

!Bool -> Bool

==

Bool == Bool -> Bool

!=

The boolean inequality operator can also be used as an xor operator.

Bool != Bool -> Bool

&&

Boolean and operator. Right argument is only evaluated if left argument is true.

Internally left and right arguments are wrapped with anonymous functions and Bool::and is called.

Bool && Bool -> Bool

||

Boolean or operator. Right argument is only evaluated if left argument is false.

Internally left and right arguments are wrapped with anonymous functions and Bool::or is called.

Bool || Bool -> Bool

Methods

serialize

bool.serialize() -> ByteArray

show

false is turned into "false", and true is turned into "true".

bool.show() -> String

to_int

false is turned into 0, and true is turned into 1.

bool.to_int() -> Int

trace

Prints a message while returning the Bool value itself. This can be convenient when debugging the outcome of a script.

bool.trace(msg: String) -> Bool

The msg is prefixed to either "true" or "false".

ByteArray

Represents an array of bytes (i.e. an array of uint8 numbers).

byte_array = #213212; ...

Note: in Haskell/Plutus ByteArray is called a ByteString, but that name was deemed too ambiguous for average programmers so ByteArray was chosen instead.

Associated functions

from_data

ByteArray::from_data(data: Data) -> ByteArray

Getters

length

Returns the number of bytes in the ByteArray.

byte_array.length -> Int 

Operators

==

ByteArray == ByteArray -> Bool

!=

ByteArray != ByteArray -> Bool

>=

ByteArray >= ByteArray -> Bool

>

The lhs is greater-than the rhs if the first rhs byte, that isn't equal to the corresponding lhs byte, is smaller than that byte. Returns true if all common bytes are equal, but the rhs is shorter than the lhs.

ByteArray > ByteArray -> Bool

<=

ByteArray <= ByteArray -> Bool

<

The lhs is less-than the rhs if the first rhs byte, that isn't equal to the corresponding lhs byte, is greater than that byte. Returns false if the rhs is empty.

ByteArray < ByteArray -> Bool

+

Concatenation of two ByteArrays.

ByteArray + ByteArray -> ByteArray

Methods

blake2b

Calculates the blake2b-256 hash of a ByteArray. The result is 32 bytes long.

byte_array.blake2b() -> ByteArray

decode_utf8

Turns a valid sequence of utf-8 bytes into a String. Throws an error if the ByteArray isn't valid utf-8.

byte_array.decode_utf8() -> String

ends_with

Checks if a ByteArray ends with a given suffix.

byte_array.ends_with(suffix: ByteArray) -> Bool

serialize

byte_array.serialize() -> ByteArray

sha2

Calculates the sha2-256 hash of a ByteArray. The result is 32 bytes long.

byte_array.sha2() -> ByteArray

sha3

Calculates the sha3-256 hash of a ByteArray. The result is 32 bytes long.

byte_array.sha3() -> ByteArray

show

Converts a ByteArray into its hexadecimal representation.

byte_array.show() -> String

slice

byte_array.slice(start: Int, end: Int) -> ByteArray

starts_with

Checks if a ByteArray starts with a given prefix.

byte_array.starts_with(prefix: ByteArray) -> Bool

Credential

Represents the non-staking part of an Address. Internally represented as an enum with two variants:


Example instantiation:

pubkey_credential: Credential::PubKey = Credential::new_pubkey(PubKeyHash::new(#...));

validator_credential: Credential::Validator = Credential::new_validator(ValidatorHash::new(#...)); ...

Associated functions

new_pubkey

Credential::new_pubkey(pkh: PubKeyHash) -> Credential::PubKey

new_validator

Credential::new_validator(vh: ValidatorHash) -> Credential::Validator

from_data

Credential::from_data(data: Data) -> Credential

Getters

hash

Get the underlying hash.

pubkey_credential.hash -> PubKeyHash

validator_credential.hash -> ValidatorHash

Operators

==

Credential == Credential -> Bool

!=

Credential != Credential -> Bool

Methods

serialize

credential.serialize() -> ByteArray

Data

Represents type-less data, as returned by the OutputDatum::Inline inline getter. Can be cast directly into any other type using from_data, or indirectly using switch.

Getters

tag

Gets tag index of ConstrData. Throws an error if not ConstrData.

data.tag -> Int

Operators

==

Data == Data -> Bool

!=

Data != Data -> Bool

Methods

serialize

data.serialize() -> ByteArray

DatumHash

Opaque ByteArray that represents the hash of a datum.

Associated Functions

new

DatumHash::new(bytes: ByteArray) -> DatumHash

from_data

DatumHash::from_data(data: Data) -> DatumHash

Operators

==

DatumHash == DatumHash -> Bool

!=

DatumHash != DatumHash -> Bool

>=

DatumHash >= DatumHash -> Bool

>

DatumHash > DatumHash -> Bool

<=

DatumHash <= DatumHash -> Bool

<

DatumHash < DatumHash -> Bool

Methods

serialize

datum_hash.serialize() -> ByteArray

show

Hexadecimal representation of a DatumHash.

datum_hash.show() -> String

DCert

Represents an enum of staking related actions:

  • Register: register a StakingCredential
  • Deregister: deregister a StakingCredential
  • Delegate: delegate a StakingCredential to a pool
  • RegisterPool: register a pool
  • RetirePool: deregister a pool

Associated functions

from_data

DCert::from_data(data: Data) -> DCert

new_register

DCert::new_register(credential: StakingCredential) -> DCert::Register

new_deregister

DCert::new_deregister(credential: StakingCredential) -> DCert::Deregister

new_delegate

DCert::new_delegate(
	delegator: StakingCredential, 
	pool_id: PubKeyHash
) -> DCert::Delegate

new_register_pool

DCert::new_register_pool(
	pool_id: PubKeyHash, 
	pool_vfr: PubKeyHash
) -> DCert::RegisterPool

new_retire_pool

DCert::new_retire_pool(
	pool_id: PubKeyHash, 
	epoch: Int
) -> DCert::RetirePool

Getters

DCert::Register

credential

register_dcert.credential -> StakingCredential

DCert::Deregister

credential

deregister_dcert.credential -> StakingCredential

DCert::Delegate

delegator

delegate_dcert.delegator -> StakingCredential

pool_id

delegate_dcert.pool_id -> PubKeyHash

DCert::RegisterPool

pool_id

register_pool_dcert.pool_id -> PubKeyHash

pool_vrf

register_pool_dcert.pool_vrf -> PubKeyHash

DCert::RetirePool

pool_id

retire_pool_dcert.pool_id -> PubKeyHash

epoch

retire_pool_dcert.epoch -> Int

Operators

==

DCert == DCert -> Bool

!=

DCert != DCert -> Bool

Methods

serialize

dcert.serialize() -> ByteArray

Duration

The difference of two Time values is a Duration value. Only a Duration can be added to a Time (two Time values can't be added).

Associated Functions

new

Instantiate a Duration from a number of milliseconds.

Duration::new(milliseconds: Int) -> Duration

from_data

Duration::from_data(data: Data) -> Duration

Operators

==

Duration == Duration -> Bool

!=

Duration != Duration -> Bool

>=

Duration >= Duration -> Bool

>

Duration > Duration -> Bool

<=

Duration <= Duration -> Bool

<

Duration < Duration -> Bool

+

Duration + Duration -> Duration

-

Duration - Duration -> Duration

*

Duration * Int -> Duration

/

A Duration divided by a Duration is an Int.

Duration / Duration -> Int

A Duration divided by an Int is a Duration.

Duration / Int -> Duration

%

Module operator that calculates remainder upon division.

Duration % Duration -> Duration

Methods

serialize

duration.serialize() -> ByteArray

Int

This is an unbounded integer (like Haskell's Integer type).

Associated functions

from_data

Int::from_data(data: Data) -> Int

from_little_endian

Converts a ByteArray into an Int. The first byte is multiplied by 1 before adding to the sum, the second byte is multiplied by 256 etc.

Int::from_little_endian(bytes: ByteArray) -> Int

parse

Parses a string representation of an integer of the form ((-)?[1-9][0-9]*)|0 (i.e. a non zero-padded integer). Throws an error if the string representation of the integer doesn't respect this format. Note that -0 isn't allowed, so zeros can only be represented by a single 0 digit.

Int::parse(string: String) -> Int

max

Returns the greater of two numbers.

Int::max(a: Int, b: Int) -> Int

min

Returns the lesser of two numbers.

Int::min(a: Int, b: Int) -> Int

Operators

==

Int == Int -> Bool

!=

Int != Int -> Bool

>=

Int >= Int -> Bool

>

Int > Int -> Bool

<=

Int <= Int -> Bool

<

Int < Int -> Bool

+

Int + Int -> Int

-

Int - Int -> Int

*

Int * Int -> Int

/

Int / Int -> Int

%

Modulo operator that returns remained after division.

Int % Int -> Int

Methods

bound

Bounds an Int if it falls outside a range. This builtin function doesn't check of the range is valid.

int.bound(low: Int, high: Int) -> Int

bound_min

Bounds an Int to be greater or equals to a given minimum value.

int.bound_min(low: Int) -> Int

bound_max

Bounds an Int to be less or equals to a given maximum value.

int.bound_max(high: Int) -> Int

serialize

int.serialize() -> ByteArray

show

Returns decimal representation of integer.

int.show() -> String

to_bool

Turns 0 into false, and any other integer into true.

int.to_bool() -> Bool

to_hex

Returns hexadecimal representation of integer.

int.to_hex() -> String

List

Helios linked-list type.

The syntax for a list type uses empty brackets followed by a type parameter: []ItemType.

Example:

example: []Int = []Int{1, 2, 3, 4, 5}; ...

Looping over multiple lists at once can be done with a recursive function call:

func add_element_wise(a: []Int, b: []Int) -> []Int {
    if (a.is_empty()) {
        []Int{}
    } else {
        add_element_wise(a.tail, b.tail).prepend(a.head + b.head)
    }
}

Associated Functions

from_data

[]ItemType::from_data(data: Data) -> []ItemType

new

Creates a new list of length n, where every contained item is determined by fn(i: Int) (i is the 0-based index of the item).

[]ItemType::new(n: Int, fn: (i: Int) -> ItemType) -> []ItemType

new_const

Creates a new list of length n, where very contained item is the same.

[]ItemType::new_const(n: Int, item: ItemType) -> []ItemType

Getters

length

Returns the length of a list.

list.length -> Int

Returns the first item in a list. Throws an error if the list is empty.

list.head -> ItemType

tail

Returns the list items following the first item. Throws an error if the list is empty.

list.tail -> []ItemType

Note: tail doesn't return the last item in a list but returns everything after head as a new list.

Operators

==

[]ItemType == []ItemType -> Bool

!=

[]ItemType != []ItemType -> Bool

+

List concatenation

[]ItemType + []ItemType -> []ItemType

Methods

all

Returns true if all of the items in the list satisfy the predicate.

list.all(predicate: (ItemType) -> Bool) -> Bool

any

Returns true if any of the items in the list satisfy the predicate.

list.any(predicate: (ItemType) -> Bool) -> Bool

filter

Returns a list of all the items in the old list that satisfy the predicate.

list.filter(predicate: (ItemType) -> Bool) -> []ItemType

find

Returns the first item in the list that satisfies the predicate. Throws an error if no item satisfies the predicate.

list.find(predicate: (ItemType) -> Bool) -> ItemType

find_safe

Returns the first item in the list that satisfies the predicate, wrapped in an Option. Returns a Option[ItemType]::None if no items match the predicate.

list.find_safe(predicate: (ItemType) -> Bool) -> Option[ItemType]

fold

Folds a list into a single value by continuosly applying the binary function to the items of the list. The result type is a type parameter of this method: ReducedType.

list.fold(
    reducer: (ReducedType, ItemType) -> ReducedType, 
    init: ReducedType
) -> ReducedType

fold_lazy

Fold that allows breaking the loop before reaching the end of the list. Can also be used to fold from the last to the first entry of the list instead of the other way around.

list.fold_lazy(
    reducer: (item: ItemType, next: () -> ReducedType) -> ReducedType,
    final: ReducedType
) -> ReducedType

for_each

Print or assert something for each item. Returns void.

list.for_each(fn: (item: ItemType) -> ()) -> ()

get

Returns the item at the given position in the list (0-based index). Throws an error if the index is out of range.

list.get(index: Int) -> ItemType

Note: get(n) has to iterate until it encounters the n-th item, so this method is O(n) and not O(1).

is_empty

Returns true if the list is empty.

list.is_empty() -> Bool

map

Transforms each item of a list. The resulting list item type is a type parameter of this method: NewItemType.

list.map(mapper: (ItemType) -> NewItemType) -> []NewItemType

prepend

Creates a new list by prepending an item to the old list.

list.prepend(item: ItemType) -> []ItemType

serialize

list.serialize() -> ByteArray

sort

Sorts the list using insertion sort.

list.sort((a: ItemType, b: ItemType) -> Bool) -> []ItemType

Map

List of key-value pairs. The insertion order of the key-value pairs matters.

Note: a Map is internally not implemented as a hash-table, so keys aren't guaranteed to be unique.

The map type syntax takes two type parameters: Map[KeyType]ValueType.

Example:

my_map = Map[String]Int{"zero": 0, "one": 1, "two": 2};
print(my_map.get("zero").show()); ... // prints "0"

Associated Functions

from_data

Map[KeyType]ValueType::from_data(data: Data) -> Map[KeyType]ValueType

Getters

head_key

Returns the key of the first entry in the Map. Throws an error if the Map is empty.

map.head_key -> KeyType

head_value

Returns the value of the first entry in the Map. Throws an error if the Map is empty.

map.head_value -> ValueType

length

Returns the number of items in a map.

map.length -> Int

tail

Returns the entries after the first entry as a new Map. Throws an error if the Map is empty.

map.tail -> Map[KeyType]ValueType

Operators

==

Map[KeyType]ValueType == Map[KeyType]ValueType -> Bool

Note: because Plutus-Core handles Map as a list, the entries must be in the same order for == to return true.

!=

Map[KeyType]ValueType != Map[KeyType]ValueType -> Bool

+

Concatenation of two maps.

Map[KeyType]ValueType + Map[KeyType]ValueType -> Map[KeyType]ValueType

Methods

all

Returns true if all map entries satisfy the predicate.

map.all(predicate: (KeyType, ValueType) -> Bool) -> Bool

any

Returns true if any map entry satisfies the predicate.

map.any(predicate: (KeyType, ValueType) -> Bool) -> Bool

delete

Removes all entries with the given key. Doesn't throw an error if key isn't found.

map.delete(key: KeyType) -> Map[KeyType]ValueType

filter

map.filter(predicate: (KeyType, ValueType) -> Bool) -> Map[KeyType]ValueType

find

Returns the key and value of the first entry that matches the predicate. Throws an error if none found.

map.find(predicate: (KeyType, ValueType) -> Bool) -> (KeyType, ValueType)

find_safe

Returns a callback (that returns the found key-value pair) and a Bool. Calling the callback if the returned Bool is false throws an error.

map.find_safe(
    predicate: (KeyType, ValueType) -> Bool
) -> (() -> (KeyType, ValueType), Bool)

find_key

Returns the first key that matches the predicate. Throws an error if none found.

map.find_key(predicate: (KeyType) -> Bool) -> KeyType

find_key_safe

Returns an Option containing the first key that matches the predicate, or Option[KeyType]::None if none found.

map.find_key_safe(predicate: (KeyType) -> Bool) -> Option[KeyType]

find_value

Returns the first value that matches the predicate. Throws an error if none found.

map.find_value(predicate: (ValueType) -> Bool) -> ValueType

find_value_safe

Returns an Option containing the first value that matches the predicate, or Option[ValueType]::None if none found.

map.find_value_safe(predicate: (ValueType) -> Bool) -> Option[ValueType]

fold

map.fold(
    reducer: (ReducedType, KeyType, ValueType) -> ReducedType, 
    init: ReducedType
) -> ReducedType

fold_lazy

Fold that allows breaking the loop before reaching the end of the map. Can also be used to fold from the last to the first entry of the Map instead of the other way around.

map.fold_lazy(
    reducer: (KeyType, ValueType, next: () -> ReducedType) -> ReducedType,
    final: ReducedType
) -> ReducedType

for_each

Print or assert something for each map entry. Returns void.

map.for_each(fn: (key: KeyType, value: ValueType) -> ()) -> ()

get

Returns the value of the first entry in the map that matches the given key. Throws an error of the key isn't found.

map.get(key: KeyType) -> ValueType

get_safe

Returns the value of the first entry in the map that matches the given key (wrapped in an option). Returns Option[ValueType]::None if the key isn't found.

map.get_safe(key: KeyType) -> Option[ValueType]

head

Get the key and the value of the first entry.

map.head() -> (KeyType, ValueType)

map

Creates a new map by transforming the map keys and values.

map.map(
    mapper: (KeyType, ValueType) -> (NewKeyType, NewValueType)
) -> Map[NewKeyType]NewValueType

prepend

Prepends a key-value pair to the beginning of the Map, creating a new Map.

map.prepend(key: KeyType, value: ValueType) -> Map[KeyType][ValueType]

serialize

map.serialize() -> ByteArray

set

Sets the first entry with given key to a new value. This entry is appended to end of the Map if the key isn't found.

map.set(key: KeyType, value: ValueType) -> Map[KeyType]ValueType

sort

Sorts the map using insertion sort. The comparison function should return true if a and b are in the correct order.

map.sort(
    compare: (
        key_a: KeyType, value_a: ValueType, 
        key_b: KeyType, value_b: ValueType
    ) -> Bool
) -> Map[KeyType]ValueType

MintingPolicyHash

Opaque ByteArray that represents the hash of a minting policy script.

Example:

mph: MintingPolicyHash = MintingPolicyHash::new(#...); ...

Associated Functions

new

MintingPolicyHash::new(bytes: ByteArray) -> MintingPolicyHash

from_data

MintingPolicyHash::from_data(data: Data) -> MintingPolicyHash

from_script_hash

Casts the generic ScriptHash type into MintingPolicyHash.

MintingPolicyHash::from_script_hash(hash: ScriptHash) -> MintingPolicyHash

Operators

==

MintingPolicyHash == MintingPolicyHash -> Bool

!=

MintingPolicyHash != MintingPolicyHash -> Bool

>=

MintingPolicyHash >= MintingPolicyHash -> Bool

>

MintingPolicyHash > MintingPolicyHash -> Bool

<=

MintingPolicyHash <= MintingPolicyHash -> Bool

<

MintingPolicyHash < MintingPolicyHash -> Bool

Methods

serialize

mph.serialize() -> ByteArray

show

Hexadecimal representation of MintingPolicyHash.

mph.show() -> String

Option

Option[SomeType] is an enum used to represent an optional value. Its type syntax takes one type parameter. An option has two variants:

  • Some
  • None

Example:

option_some: Option[Int] = Option[Int]::Some{42};
option_none: Option[Int] = Option[Int]::None; ...

Associated Functions

from_data

Option[SomeType]::from_data(data: Data) -> Option[SomeType]

Getters

Option[SomeType]::Some

some

Returns content of Option[SomeType]::Some.

option_some.some -> SomeType

Note: this getter doesn't exist on Option[SomeType]::None.

Operators

==

Option[SomeType] == Option[SomeType] -> Bool

!=

Option[SomeType] != Option[SomeType] -> Bool

Methods

map

Maps None to None and Some to Some.

option.map(fn: (some: OldSomeType) -> NewSomeType) -> Option[NewSomeType]

serialize

option.serialize() -> ByteArray

unwrap

Returns the value wrapped by Some. Throws an error if None.

option.unwrap() -> SomeType

OutputDatum

Represents that datum data of a TxOutput instance.

OutputDatum is an enum with 3 variants:

  • None
  • Hash
  • Inline

Associated functions

from_data

OutputDatum::from_data(data: Data) -> OutputDatum

new_hash

Construct a new OutputDatum::Hash instance. Only available after main, see script structure.

OutputDatum::new_hash(datum_hash: DatumHash) -> OutputDatum::Hash

new_inline

Construct a new OutputDatum::Inline instance from any value. Only available after main, see script structure.

OutputDatum::new_inline(any: AnyType) -> OutputDatum::Inline

new_none

Construct a new OutputDatum::None instance. Only available after main, see script structure.

OutputDatum::new_none() -> OutputDatum::None

Getters

OutputDatum

get_inline_data

Short-hand for output_datum.switch{inline: Inline => inline.data, else => error("not an inline datum")}:

output_datum.get_inline_data() -> Data

OutputDatum::Hash

hash

hash_output_datum.hash -> DatumHash

OutputDatum::Inline

data

inline_output_datum.data -> Data

Use the from_data associated function, which is automatically defined on every type, to turn Data into another type.

Operators

==

OutputDatum == OutputDatum -> Bool

!=

OutputDatum != OutputDatum -> Bool

Methods

serialize

output_datum.serialize() -> ByteArray

PubKey

Opaque ByteArray that represents a Ed25519 public key.

A PubKey is 32 bytes long. A PubKeyHash is the blake2b-224 hash of a PubKey. Sadly there is no on-chain way of calculating the PubKeyHash from a PubKey (only blake2b-256 is available).

Example instantiation:

pub_key: PubKey = PubKey::new(#...); ...

Associated Functions

new

PubKey::new(bytes: ByteArray) -> PubKey

from_data

PubKey::from_data(data: Data) -> PubKey

Operators

==

PubKey == PubKey -> Bool

!=

PubKey != PubKey -> Bool

Methods

serialize

pub_key.serialize() -> ByteArray

show

Hexadecimal representation of a PubKey.

pub_key.show() -> String

verify

Verify the signature of a message (using Ed25519).

pub_key.verify(message: ByteArray, signature: ByteArray) -> Bool

The signature is expected to be 64 bytes long.

PubKeyHash

Opaque ByteArray that represents the hash of a PubKey.

The first part of a regular payment address (i.e. not witnessed by a script) is a PubKeyHash.

Example instantiation:

pkh: PubKeyHash = PubKeyHash::new(#...); ...

Associated Functions

new

PubKeyHash::new(bytes: ByteArray) -> PubKeyHash

from_data

PubKeyHash::from_data(data: Data) -> PubKeyHash

Operators

==

PubKeyHash == PubKeyHash -> Bool

!=

PubKeyHash != PubKeyHash -> Bool

>=

PubKeyHash >= PubKeyHash -> Bool

>

PubKeyHash > PubKeyHash -> Bool

<=

PubKeyHash <= PubKeyHash -> Bool

<

PubKeyHash < PubKeyHash -> Bool

Methods

serialize

pkh.serialize() -> ByteArray

show

Hexadecimal representation of a PubKeyHash.

pkh.show() -> String

ScriptContext

The ScriptContext contains all the metadata related to a signed Cardano transaction and is often an important argument of the validator script main function.

It wraps the Tx type and provides some extra methods.

Associated functions

from_data

ScriptContext::from_data(data: Data) -> ScriptContext

new_certifying

Construct a ScriptContext instance with a staking/certifying ScriptPurpose. Only available after main, see script structure.

Throws an error if the current script purpose isn't staking or testing.

ScriptContext::new_certifying(
    tx:    Tx,
    dcert: DCert
) -> ScriptContext

new_minting

Construct a ScriptContext instance with a minting ScriptPurpose. Only available after main, see script structure.

Throws an error if the current script purpose isn't minting or testing.

ScriptContext::new_minting(
    tx:  Tx,
    mph: MintingPolicyHash
) -> ScriptContext

new_rewarding

Construct a ScriptContext instance with a staking/rewarding ScriptPurpose. Only available after main, see script structure.

Throws an error if the current script purpose isn't staking or testing.

ScriptContext::new_rewarding(
    tx: Tx,
    sc: StakingCredential
) -> ScriptContext

new_spending

Construct a ScriptContext instance with a spending ScriptPurpose. Only available after main, see script structure.

Throws an error if the current script purpose isn't spending or testing.

ScriptContext::new_spending(
    tx:        Tx,
    output_id: TxOutputId
) -> ScriptContext

Getters

tx

Get the Tx data structure.

ctx.tx -> Tx

Operators

==

ScriptContext == ScriptContext -> Bool

!=

ScriptContext != ScriptContext -> Bool

Methods

serialize

Returns the cbor-serialization of the ScriptContext.

ctx.serialize() -> ByteArray

get_spending_purpose_output_id

Returns the TxOutputId of the current UTxO being spent.

Can only be called in spending purpose scripts, and throws an error otherwise.

ctx.get_spending_purpose_output_id() -> TxOutputId

get_current_input

Returns the current UTxO being spent as a TxInput.

Can only be called in spending purpose scripts, and throws an error otherwise.

ctx.get_current_input() -> TxInput

get_cont_outputs

Returns the outputs sent back to the current validator script.

Can only be called in spending purpose scripts, and throws an error otherwise.

ctx.get_cont_outputs() -> []TxOutput

get_current_validator_hash

Returns the ValidatorHash of the current script.

Can only be called in spending purpose scripts, and throws an error otherwise.

ctx.get_current_validator_hash() -> ValidatorHash

get_current_minting_policy_hash

Returns the MintingPolicyHash of the minting policy being evaluated.

Can only be called in minting purpose scripts, and throws an error otherwise.

ctx.get_current_minting_policy_hash() -> MintingPolicyHash

get_staking_purpose

Returns the current StakingPurpose (Rewarding or Certifying).

Can only be called in staking purpose scripts, and throws an error otherwise.

ctx.get_staking_purpose() -> StakingPurpose

ScriptHash

Opaque ByteArray that represents either a ValidatorHash, a MintingPolicyHash, or a StakingValidatorHash.

This is returned by the TxOutput.ref_script_hash getter (a reference script can be any of the above script types).

Associated functions

from_data

ScriptHash::from_data(data: Data) -> ScriptHash

Operators

==

ScriptHash == ScriptHash -> Bool

!=

ScriptHash != ScriptHash -> Bool

Methods

serialize

script_hash.serialize() -> ByteArray

ScriptPurpose

Each redemption in a transaction has a ScriptPurpose with the following 4 variants:

  • Minting
  • Spending
  • Rewarding
  • Certifying

ScriptPurpose::Rewarding and ScriptPurpose::Certifying are identical to StakingPurpose::Rewarding and StakingPurpose::Certifying respectively, but the use cases are different. StakingPurpose is used for switching between rewarding and certifying within a given staking script. ScriptPurpose is used to see what other scripts are being used in the same transaction (see tx.redeemers).

Associated functions

from_data

ScriptPurpose::from_data(data: Data) -> ScriptPurpose

new_minting

ScriptPurpose::new_minting(mph: MintingPolicyHash) -> ScriptPurpose::Minting

new_spending

ScriptPurpose::new_spending(output_id: TxOutputId) -> ScriptPurpose::Spending

new_rewarding

ScriptPurpose::new_rewarding(staking_credential: StakingCredential) -> ScriptPurpose::Rewarding

new_certifying

ScriptPurpose::new_certifying(dcert: DCert) -> ScriptPurpose::Certifying

Getters

ScriptPurpose::Minting

policy_hash

Returnt the MintingPolicyHash of the UTxO whose minting or burning is being validated.

minting_script_purpose.policy_hash -> MintingPolicyHash

ScriptPurpose::Spending

output_id

Returns the TxOutputId of the UTxO whose spending is being validated.

spending_script_purpose.output_id -> TxOutputId

ScriptPurpose::Rewarding

credential

Returns the StakingCredential for which rewards are being withdrawn.

rewarding_script_purpose.credential -> StakingCredential

ScriptPurpose::Certifying

dcert

Returns the current stake certifying action as a DCert.

certifying_script_purpose.dcert -> DCert

Operators

==

ScriptPurpose == ScriptPurpose -> Bool

!=

ScriptPurpose != ScriptPurpose -> Bool

Methods

serialize

script_purpose.serialize() -> ByteArray

StakeKeyHash

Opaque ByteArray that represents the hash of a staking key.

Example instantiation:

skh: StakeKeyHash = StakeKeyHash::new(#...); ...

Associated Functions

new

StakeKeyHash::new(bytes: ByteArray) -> StakeKeyHash

from_data

StakeKeyHash::from_data(data: Data) -> StakeKeyHash

Operators

==

StakeKeyHash == StakeKeyHash -> Bool

!=

StakeKeyHash != StakeKeyHash -> Bool

>=

StakeKeyHash >= StakeKeyHash -> Bool

>

StakeKeyHash > StakeKeyHash -> Bool

<=

StakeKeyHash <= StakeKeyHash -> Bool

<

StakeKeyHash < StakeKeyHash -> Bool

Methods

serialize

skh.serialize() -> ByteArray

show

Hexadecimal representation of a StakeKeyHash.

skh.show() -> String

StakingCredential

Represents the staking part of an Address.

StakingCredential is an enum with 2 variants:

  • Hash
  • Ptr

Associated functions

new_hash

Constructs a new StakingCredential from StakingHash (which in turn is an enum that represents StakeKeyHash/StakingValidatorHash).

StakingCredential::new_hash(staking_hash: StakingHash) -> StakingCredential::Hash

new_ptr

StakingCredential::new_ptr(a: Int, b: Int, c: Int) -> StakingCredential::Ptr

from_data

StakingCredential::from_data(data: Data) -> StakingCredential

Getters

StakingCredential::Hash

hash

Get the underlying StakingHash.

staking_credential_hash.hash -> StakingHash

The following example code can be used to extract the underlying StakingValidatorHash:

staking_credential.switch{
  h: Hash => h.hash.switch{
    v: Validator => v.hash,
    else => error("not a StakingHash::Validator")
  }, 
  else => error("not a StakingCredential::Hash")
}

Operators

==

StakingCredential == StakingCredential -> Bool

!=

StakingCredential != StakingCredential -> Bool

Methods

serialize

staking_credential.serialize() -> ByteArray

StakingHash

An enum with two variants:


Example instantiation:

stakekey_stakinghash: StakingHash::StakeKey = StakingHash::new_stakekey(StakeKeyHash::new(#...));

validator_stakinghash: StakingHash::Validator = StakingHash::new_validator(StakingValidatorHash::new(#...)); ...

Associated functions

new_stakekey

StakingHash::new_stakekey(skh: StakeKeyHash) -> StakingHash::StakeKey

new_validator

StakingHash::new_validator(svh: StakingValidatorHash) -> StakingHash::Validator

from_data

StakingHash::from_data(data: Data) -> StakingHash

Getters

hash

Get the underlying hash.

stakekey_stakinghash.hash -> StakeKeyHash

validator_stakinghash.hash -> StakingValidatorHash

Operators

==

StakingHash == StakingHash -> Bool

!=

StakingHash != StakingHash -> Bool

Methods

serialize

stakinghash.serialize() -> ByteArray

StakingPurpose

A staking purpose script has a StakingPurpose, which is an enum with 2 variants:

  • Rewarding
  • Certifying

Associated functions

from_data

StakingPurpose::from_data(data: Data) -> StakingPurpose

Getters

StakingPurpose::Rewarding

credential

Returns the StakingCredential for which rewards are being withdrawn.

rewarding_staking_purpose.credential -> StakingCredential

StakingPurpose::Certifying

dcert

Returns the current stake certifying action as a DCert.

certifying_staking_purpose.dcert -> DCert

Operators

==

StakingPurpose == StakingPurpose -> Bool

!=

StakingPurpose != StakingPurpose -> Bool

Methods

serialize

staking_purpose.serialize() -> ByteArray

StakingValidatorHash

Opaque ByteArray that represents the hash of a staking script.

Associated functions

new

StakingValidatorHash::new(bytes: ByteArray) -> StakingValidatorHash

from_data

StakingValidatorHash::from_data(data: Data) -> StakingValidatorHash

from_script_hash

Casts the generic ScriptHash type into StakingValidatorHash.

StakingValidatorHash::from_script_hash(hash: ScriptHash) -> StakingValidatorHash

Operators

==

StakingValidatorHash == StakingValidatorHash -> Bool

!=

StakingValidatorHash != StakingValidatorHash -> Bool

>=

StakingValidatorHash >= StakingValidatorHash -> Bool

>

StakingValidatorHash > StakingValidatorHash -> Bool

<=

StakingValidatorHash <= StakingValidatorHash -> Bool

<

StakingValidatorHash < StakingValidatorHash -> Bool

Methods

serialize

staking_validator_hash.serialize() -> ByteArray

show

Hexadecimal representation of the StakingValidatorHash.

staking_validator_hash.show() -> String

String

Represents a piece of utf-8 text.

string: String = "Woah!"; ...

Associated functions

from_data

String::from_data(data: Data) -> String

Operators

==

String == String -> Bool

!=

String != String -> Bool

+

String concatenation.

String + String -> String

Methods

serialize

string.serialize() -> ByteArray

encode_utf8

Turns a String into a sequence of utf-8 bytes.

string.encode_utf() -> ByteArray

starts_with

Checks if a String starts with a given prefix.

string.starts_with(prefix: String) -> Bool

ends_with

Checks if a String ends with a given suffix.

string.ends_with(suffix: String) -> Bool

Time

Represents POSIX time in milliseconds (time since 1970/01/01 00:00:00 UTC).

Associated Functions

new

Time::new(millis_since_1970: Int) -> Time

from_data

Time::from_data(data: Data) -> Time

Operators

==

Time == Time -> Bool

!=

Time != Time -> Bool

>=

Time >= Time -> Bool

>

Time > Time -> Bool

<=

Time <= Time -> Bool

<

Time < Time -> Bool

+

Time + Duration -> Time

-

Subtraction a Duration from a Time is like adding a negative Duration.

Time - Duration -> Time

The difference of two Times is a Duration.

Time - Time -> Duration

Methods

serialize

time.serialize() -> ByteArray

show

Decimal representation of the underlying raw Int.

time.show() -> String

TimeRange

This represents a range of time using a pair of Time values, or open ends.

Associated functions and constants

ALWAYS

Represents a TimeRange going from negative to positive infinity, thus contains all possible Time values.

TimeRange::ALWAYS -> TimeRange

NEVER

Represents TimeRange going from positive to negative infinity. It contains nothing as it's an impossible range.

TimeRange::NEVER -> TimeRange

from

Returns a TimeRange that contains all Time values from start onwards.

TimeRange::from(start: Time) -> TimeRange

to

Returns a TimeRange that contains all Time values before end.

TimeRange::to(end: Time) -> TimeRange

new

Returns a TimeRange that contains all Time values between start and end.

TimeRange::new(start: Time, end: Time) -> TimeRange

from_data

TimeRange::from_data(data: Data) -> TimeRange

Getters

start

Returns the start Time of a TimeRange. Throws an error if start is non-finite.

time_range.start -> Time

end

Returns the end Time of a TimeRange. Throws an error if end is non-finite.

time_range.end -> Time

Operators

==

TimeRange == TimeRange -> Bool

!=

TimeRange != TimeRange -> Bool

Methods

serialize

time_range.serialize() -> ByteArray

contains

Returns true if a TimeRange contains the given time.

time_range.contains(time: Time) -> Bool

is_before

Returns true if the end of a TimeRange is before the given time. Always returns false if the end of the TimeRange is positive infinity.

time_range.is_before(time: Time) -> Bool

is_after

Returns true if the start of a TimeRange is after the given time. Always returns false if the start of the TimeRange is negative infinity.

time_range.is_after(time: Time) -> Bool

Tx

Represents a balanced transaction.

Associated functions

from_data

Tx::from_data(data: Data) -> Tx

new

Construct a Tx instance. Only available after main, see script structure.

Tx::new(
    inputs:      []TxInput,
    ref_inputs:  []TxInput,
    outputs:     []TxOutput,
    fee:         Value,
    minted:      Value,
    dcerts:      []DCert,
    withdrawals: Map[StakingCredential]Int,
    time_range:  TimeRange,
    signatories: []PubKeyHash,
    redeemers:   Map[ScriptPurpose]AnyType,
    datums:      Map[DatumHash]AnyType
) -> Tx

Note: the value type of the redeemers and datums fields can be any type when instantiating a new Tx instance. But when getting the redeemers and the datums the value type is actually Data (see redeemers and datums).

Getters

inputs

Returns the list of TxInputs of the transaction.

tx.inputs -> []TxInput

ref_inputs

Returns the list of reference inputs (as []TxInput) of the transaction.

tx.ref_inputs -> []TxInput

outputs

Returns the list of TxOutputs of the transaction.

tx.outputs -> []TxOutput

fee

Returns the fee Value paid for the transaction.

tx.fee -> Value

minted

Returns the Value minted by the transaction.

tx.minted -> Value

dcerts

Returns the list of DCerts of the transaction (i.e. list of staking certifying actions).

tx.dcerts -> []DCert

withdrawals

Returns a map of staking reward withdrawals. The map value Ints are lovelace quantities.

tx.withdrawals -> Map[StakingCredential]Int

time_range

Returns the valid TimeRange of the transaction. This TimeRange must contain the current time.

tx.time_range -> TimeRange

Note: we can't access the current time from within the validator script because it would lead to differing evaluation results as the tx propagates across the network. Instead we can use tx.time_range as an approximation of the current time.

signatories

Returns the list of explicit transaction signers as []PubKeyHash.

tx.signatories -> []PubKeyHash

redeemers

Returns all the redeemers of the transaction as a map with ScriptPurpose keys, and Data values. This allows more complex interactions between different scripts being used in the same transaction.

tx.redeemers -> Map[ScriptPurpose]Data

datums

Returns a Map of DatumHashes to raw Data. This can be used to get the datum content of any TxInput that doesn't use inline datums.

tx.datums -> Map[DatumHash]Data

id

Returns the hash of the current transaction as TxId.

tx.id -> TxId

Operators

==

Tx == Tx -> Bool

!=

Tx != Tx -> Bool

Methods

serialize

tx.serialize() -> ByteArray

is_signed_by

Returns true if the transaction was signed by the given pubkeyhash.

tx.is_signed_by(pubkeyhash: PubKeyHash) -> Bool

find_datum_hash

Returns the DatumHash of datum data used in one the UTxO inputs.

tx.find_datum_hash(data: AnyType) -> ByteArray

get_datum_data

Returns the datum Data of a TxOutput. Throws an error if no datum is attached to the output.

tx.get_datum_data(output: TxOutput) -> Data

outputs_sent_to

Returns the TxOutputs sent to a regular payment address.

tx.outputs_sent_to(pkh: PubKeyHash) -> []TxOutput

outputs_sent_to_datum

Returns the TxOutputs sent to a regular payment address tagged with the given datum (datum tagging can be used to prevent double satisfaction exploits).

tx.outputs_sent_to_datum(
    pkh: PubKeyHash, 
    datum: AnyType, 
    is_inline: Bool
) -> []TxOutput

outputs_locked_by

Returns the TxOutputs being locked at the given script address.

tx.outputs_locked_by(script_hash: ValidatorHash) -> []TxOutput

outputs_locked_by_datum

Returns the TxOutputs being locked at the given script address with the given datum.

tx.outputs_locked_by_datum(
    script_hash: ValidatorHash, 
    datum: AnyType, 
    is_inline: Bool
) -> []TxOutput

value_sent_to

Returns the output Value sent to a regular payment address.

tx.value_sent_to(addr: PubKeyHash) -> Value

value_sent_to_datum

Returns the output Value sent to a regular payment address tagged with the given datum (datum tagging can be used to prevent double satisfaction exploits).

tx.value_sent_to_datum(
    addr: PubKeyHash, 
    datum: AnyType, 
    is_inline: Bool
) -> Value

value_locked_by

Returns the output Value being locked at the given script address.

tx.value_locked_by(script_hash: ValidatorHash) -> Value

value_locked_by_datum

Returns the output Value being locked at the given script address with the given datum.

tx.value_locked_by_datum(
    script_hash: ValidatorHash, 
    datum: AnyType, 
    is_inline: Bool
) -> Value

TxId

This is a type-safe wrapper around ByteArray representing the hash of a transaction.

Associated functions

new

TxId::new(bytes: ByteArray) -> TxId

from_data

TxId::from_data(data: Data) -> TxId

Operators

==

TxId == TxId -> Bool

!=

TxId != TxId -> Bool

>=

TxId >= TxId -> Bool

>

TxId > TxId -> Bool

<=

TxId <= TxId -> Bool

<

TxId < TxId -> Bool

Methods

serialize

tx_id.serialize() -> ByteArray

show

Hexadecimal representation of a TxId.

tx_id.show() -> String

TxInput

Represents a transaction input.

Associated functions

from_data

TxInput::from_data(data: Data) -> TxInput

new

Construct a TxInput instance. Only available after main, see script structure.

TxInput::new(
    output_id: TxOutputId,
    output:    TxOutput
) -> TxInput

Getters

output_id

Returns the TxOutputId of the underlying UTxO.

tx_input.output_id -> TxOutputId

output

Returns the underlying UTxO as a TxOutput.

tx_input.output -> TxOutput

Operators

==

TxInput == TxInput -> Bool

!=

TxInput != TxInput -> Bool

Methods

serialize

tx_input.serialize() -> ByteArray

TxOutput

Represents a transaction output.

Associated functions

from_data

TxOutput::from_data(data: Data) -> TxOutput

new

Construct a TxOutput instance. Only available after main, see script structure.

TxOutput::new(
    address: Address,
    value:   Value,
    datum:   OutputDatum
) -> TxOutput

Getters

address

Returns the Address at which the TxOutput is located.

tx_output.address -> Address

value

Returns the Value locked in the TxOutput.

tx_output.value -> Value

datum

Returns the datum of the TxOutput as an OutputDatum.

tx_output.datum -> OutputDatum

ref_script_hash

Returns the ScriptHash of the optional reference script attached to the TxOutput.

tx_output.ref_script_hash -> Option[ScriptHash]

Operators

==

TxOutput == TxOutput -> Bool

!=

TxOutput != TxOutput -> Bool

Methods

serialize

tx_output.serialize() -> ByteArray

TxOutputId

Represents the unique ID of a UTxO. It's composed of the transaction ID (TxId) of the transaction that created that UTxO, and of the index (Int) of that UTxO in the outputs of that transaction.

Associated functions

new

TxOutputId::new(tx_id: TxId, index: Int) -> TxOutputId

from_data

TxOutputId::from_data(data: Data) -> TxOutputId

Getters

index

Index of the UTxO in the producing transaction:

tx_output_id.index -> Int

tx_id

TxId of the producing transaction:

tx_output_id.tx_id -> TxId

Operators

==

TxOutputId == TxOutputId -> Bool

!=

TxOutputId != TxOutputId -> Bool

>=

First compares bytes of TxId, then compares index.

TxOutputId >= TxOutputId -> Bool

>

First compares bytes of TxId, then compares index.

TxOutputId > TxOutputId -> Bool

<=

First compares bytes of TxId, then compares index.

TxOutputId <= TxOutputId -> Bool

<

First compares bytes of TxId, then compares index.

TxOutputId < TxOutputId -> Bool

Methods

serialize

tx_output_id.serialize() -> ByteArray

ValidatorHash

Opaque ByteArray that represents the hash of a validator script.

The first part of a script address is formed by a ValidatorHash.

Associated functions

new

ValidatorHash::new(bytes: ByteArray) -> ValidatorHash

from_data

ValidatorHash::from_data(data: Data) -> ValidatorHash

from_script_hash

Casts the generic ScriptHash type into ValidatorHash.

ValidatorHash::from_script_hash(hash: ScriptHash) -> ValidatorHash

Operators

==

ValidatorHash == ValidatorHash -> Bool

!=

ValidatorHash != ValidatorHash -> Bool

>=

ValidatorHash >= ValidatorHash -> Bool

>

ValidatorHash > ValidatorHash -> Bool

<=

ValidatorHash <= ValidatorHash -> Bool

<

ValidatorHash < ValidatorHash -> Bool

Methods

serialize

validator_hash.serialize() -> ByteArray

show

Hexadecimal representation of the ValidatorHash.

validator_hash.show() -> String

Value

The Value type represents monetary value as a token bundle (internally represented as a Map[MintingPolicyHash]Map[ByteArray]Int)

Note: 1 ADA is equal to 1 million Lovelace

Note: You might find yourself comparing the output of value.get() to a number in order to check if value contains something, but in that case it is usually better to use the value.contains() method instead.

Associated functions and constants

ZERO

An empty Value.

Value::ZERO -> Value

lovelace

Returns a Value containing only lovelace.

Value::lovelace(amount: Int) -> Value

new

Returns a Value containing an amount of a given AssetClass.

Value::new(asset_class: AssetClass, amount: Int) -> Value

from_data

Value::from_data(data: Data) -> Value

from_map

Instantiates a Value using a raw map.

Value::from_map(raw_value: Map[MintingPolicyHash]Map[ByteArray]Int) -> Value

Operators

==

Returns true if two Values are the same.

Value == Value -> Bool

Note: the assets and tokens must also be in the same order for == to return true.

!=

Value != Value -> Bool

>=

Strict greater-equals comparison. If every lhs token has a greater-or-equals amount than the equivalent rhs token then >= returns true. If any rhs token has a greater amount than the equivalent lhs token then >= returns false.

Value >= Value -> Bool

>

Strict greater-than comparison. If every lhs token has a greater amount than the equivalent rhs token then > returns true. If any rhs token has a greater-or-equals amount than the equivalent lhs token then > returns false.

Value > Value -> Bool

<=

Strict less-equals comparison. If every lhs token has a smaller-or-equals amount than the equivalent rhs token then <= returns true. If any rhs token has a smaller amount than the equivalent lhs token, or doesn't exist in lhs, then <= returns false.

Value <= Value -> Bool

<

Strict less-than comparison. If every lhs token has a smaller amount than the equivalent rhs token then < returns true. If any rhs token has a smaller-or-equals amount than the equivalent lhs token, or doesn't exist in lhs, then < returns false.

Value < Value -> Bool

+

Value + Value -> Value

-

Subtracts two Values. Note that negative token amounts are possible.

Value - Value -> Value

*

Value * Int -> Value

/

Value / Int -> Value

Methods

contains

Alias for >= (where lhs is self).

value.contains(other_value: Value) -> Bool

contains_policy

Returns true if a given MintingPolicyHash is in a Value.

value.contains_policy(mph: MintingPolicyHash) -> Bool

get

Returns the amount of the given AssetClass in a Value. Throws error if the AssetClass isn't found.

value.get(asset_class: AssetClass) -> Int

get_assets

Returns a new Value with the lovelace removed.

value.get_assets() -> Value

get_lovelace

Returns the amount of lovelace in a Value. Returns 0 if there isn't any.

value.get_lovelace() -> Int

get_safe

Like get, but returns 0 instead of throwing an error if the given AssetClass isn't found.

value.get_safe(asset_class: AssetClass) -> Int

get_policy

Returns a map of tokens of the given MintingPolicyHash in a Value. Throws an error if the MintingPolicyHash isn't found.

value.get_policy(mph: MintingPolicyHash) -> Map[ByteArray]Int

is_zero

Checks if a Value is empty.

value.is_zero() -> Bool

serialize

value.serialize() -> ByteArray

show

Returns a formatted String showing all the assets contained in a Value.

value.show() -> String

to_map

Returning the underlying Map:

value.to_map() -> Map[MintingPolicyHash]Map[ByteArray]Int

Helios API

This chapter covers how to compile Helios sources and how to build Cardano transactions using the Helios Javascript library.

Setup of the Helios Library

The Helios library is platform agnostic and can be used in many different ways.

Webpage script tag

<script src="https://helios.hyperion-bt.org/<version>/helios.js" type="module" crossorigin></script>

Module with CDN URL

Helios can be imported as a module using our CDN. This is supported by Deno and most modern browsers:

import * as helios from "https://helios.hyperion-bt.org/<version>/helios.js"

// or only the necessary parts (recommended as you get more acquainted with the library)
import { Program } from "https://helios.hyperion-bt.org/<version>/helios.js"

Alternatively you can use "helios" as a placeholder for the URL and, if not using any builder-tools, specify the module URL in an importmap (currently only supported by Chrome):

// in you javascript file
import * as helios from "helios"
<!-- in your html file -->
<script type="importmap">
    {
        "imports": {
            "helios": "https://helios.hyperion-bt.org/<version>/helios.js"
        }
    }
</script>

The examples in this chapter will use the placeholder approach.

npm

Install the latest version of the library using the following command:

$ npm i @hyperionbt/helios

Or install a specific version:

$ npm i @hyperionbt/helios@<version>

In your javascript file:

import { Program } from "@hyperionbt/helios"

We don't yet recommend installing the Helios library globally, as the API is still changing frequently.

Deno as a VSCode language server

To use Deno as a VSCode language server you must first install the Deno CLI. Assuming you have access to a Linux-linux terminal:

$ curl -fsSL https://deno.land/x/install/install.sh | sh

This should download the deno binary to $HOME/.deno/bin/deno. Either add this directory to your PATH, or copy the binary to the system-wide bin directory:

$ sudo cp $HOME/.deno/bin/deno /usr/local/bin/deno

Make sure the .vscode/settings.json file points to the correct deno binary. For example:

{
    "deno.enable": true,
    "deno.path": "/usr/local/bin/deno"
}

External modules must be cached by Deno before you can benefit from their type annotations.

Cache external modules using the following command:

$ deno cache --reload my_entry_point.js

Compiling Helios sources

The recommended way to compile Helios sources is to use the library directly. This approach makes it easier to maintain a single-source-of-truth version of your contract in client-side DApps.

First step is to write your contract as a js literal string. For example:

const src = `
spending always_succeeds

func main(_, _, _) -> Bool {
    true
}`

Then you can create a Helios Program instance:

// at top of js file
import * as helios from "helios"
...
const program = helios.Program.new(src)

The Program instantiation will perform syntax and type checking, but won't actually do the compilation into the on-chain format. For that you need to call the compile method first:

const simplify = true

const myUplcProgram = program.compile(simplify)

Note: If simplify is true the resulting program is optimized for production. If simplify is false no optimizations are performed and print expressions aren't removed, which makes the resulting program more suitable for debugging.

Here myUplcProgram is an instance of UplcProgram (uplc stands for Untyped PLutus Core). A UplcProgram instance has methods for running, profiling, hashing, and serializing the contained Plutus-Core program.

Now you can serialize the UplcProgram into a JSON string that can be used by cardano-cli:

console.log(myUplcProgram.serialize())

// prints '{"type": PlutusScriptV2, "description": "", "cborHex": ...}'

When building transactions with Helios the UplcProgram instance is used directly when attaching scripts.

Generating datums and redeemers

Smart contract transactions include datum and redeemer data. You can generate these data structures using Helios.

Let's look at the following Helios script (as a literal string inside a js file):

const src = `
spending owner_only

struct Datum {
    owner: PubKeyHash
}

func main(datum: Datum, _, ctx: ScriptContext) -> Bool {
    ctx.tx.is_signed_by(datum.owner)
}

const MY_DATUM = Datum {
    PubKeyHash::new(#...)
}`

Remember that after the main function you can define data generators and test functions (see script structure).

MY_DATUM in this example can be evaluated using the API:

// at top of js file
import * as helios from "helios"
...
const program = helios.Program.new(src)

const myDatum = program.parameters["MY_DATUM"]

Here myDatum is a UplcValue instance. UplcValue is the internal (unexported) base class of every Helios value. To get the underlying data we can use the data getter:

const myDatumData = myDatum.data

Note: the UplcValue data getter doesn't work for booleans as booleans are always kept in their primitive Plutus-Core form for performance reasons.

Here myDatumData is a UplcData instance. UplcData is equivalent to the BuiltinData type in Plutus.

To create a JSON string that can be used by cardano-cli we can use the toSchemaJson method:

console.log(myDatumData.toSchemaJson())

// prints '{"constructor": 0, "fields": [...]}'

Building transactions

Besides compiling and generating data structures the Helios library can also be used to build transactions.

Transaction building using Helios is more low-level than alternatives (eg. Lucid) as the Helios library is agnostic wrt. the query-layer, the wallet-connector, and the http api.

In this section we assume the Helios library has been imported in the following way:

import * as helios from "helios"

Tx

A new Tx instance acts as a transaction builder, using builder pattern methods.

const tx = new helios.Tx()

Overview

Transaction inputs

Each transaction input is an instance of the UTxO class. A UTxO represents a TxOutputId and, when building a new transaction, also contains the underlying TxOutput.

const utxo = new helios.UTxO(
    helios.TxId.fromHex("..."), // hash of the tx that created the utxo
    0n, // utxo index as bigint
    new helios.TxOutput(...) // TxOutput with address, value and datum fields
)

Spending a regular UTxO

Spending a regular UTxO (i.e. non-script UTxO), is done with the addInput method:

tx.addInput(utxo)

Spending a script UTxO

Spending a UTxO locked at a script address is also done with addInput, but requires specifying a redeemer:

// program.evalParam("...").data can be used directly as 'redeemerData'
tx.addInput(utxo, redeemerData)

The corresponding script must be also be attached to such a transaction:

// 'uplcProgram' is an instance of UplcProgram (i.e. result of helios.Program.new(...).compile(...))
tx.attachScript(uplcProgram)

Transaction outputs

Each transaction output is an instance of the TxOutput class. A TxOutput contains an Address, a Value, and, optionally, a Datum field.

Example: TxOutput instance without a datum

const output = new helios.TxOutput(
    helios.Address.fromBech32("addr_test..."),
    new helios.Value(1000000n), // 1 tAda == 1 million lovelace
)

Example: TxOutput instance with an inline datum

const outputWithDatum = new helios.TxOutput(
    helios.Address.fromBech32("addr_test..."),
    new helios.Value(1000000n),
    helios.Datum.inline(...), // result from program.evalParam("...").data can be used directly as an argument for Datum.inline()
)

Adding a TxOutput to a Tx

A TxOutput can be added to the transaction with the addOutput method:

tx.addOutput(output)

Multiple outputs at once with the addOutputs method:

tx.addOutputs(outputs)

Collateral

Some UTxOs must be added as collateral to the transaction in case the transaction interacts with smart contracts:

tx.addCollateral(utxo)

Note: the collateral is only lost if the transaction fails once submitted. There are however plenty of checks that happen before the transaction is submitted to the blockchain mem-pool, so such a situation is very unlikely.

Note: since v0.12.6 of Helios, setting the collateral is no longer necessary. If unset, the collateral (and collateral return) is set automatically inside tx.finalize().

Explicit signers

Explicit signers are actors who approve the transaction without necessarily sending or receiving UTxOs.

Only these explicit signers appear in the tx.signatories field.

Signers are identified by their PubKeyHash:

tx.addSigner(helios.PubKeyHash.fromHex("..."))

Minting

Tokens can be minted using the mintTokens method. The UplcProgram of the corresponding minting policy must also be attached:

tx
    .mintTokens(
        uplcProgram.mintingPolicyHash, 
        [["my_first_nft", 1n], ["my_second_nft", 1n]], 
        redeemerData // can be generated using program.evalParam("...").data
    )
    .attachScript(uplcProgram)

Note: the transaction building methods can be chained.

Finalizing

Before signing the serialized transaction using a wallet, the transaction must be finalized. The finalization process calculates the transaction fee, balances the transaction, and checks min collateral and min lovelace deposit requirements.

Finalization requires the most recent network parameters (see below), a change address for balancing, and optionally some additional UTxOs that will be used if the inputs specified by the user don't contain enough lovelace to cover the fees and deposits:

// async because scripts are evaluated asyncronously

await tx.finalize(networkParams, changeAddress, extraUTxOs)

Note: finalize is asynchronous because script evaluation is asynchronous. Script evaluation is asynchronous so that interactive debuggers can easily step through the evaluation process.

Network parameters

The finalization process require downloading the latest network parameters. For example, for the preview testnet:

// in an async context

const networkParams = new helios.NetworkParams(
    await fetch("https://d1t0d7c2nekuk0.cloudfront.net/preview.json")
        .then(response => response.json())
)

Note: we've set up a CDN with daily updated raw network parameters:

Signing and submitting a transactions

Signing

The finalized transaction can be signed by a wallet, for example using the CIP 30 DApp connector:

// in an async context

const response = await walletHandle.signTx(helios.bytesToHex(tx.toCbor()), true)

// extract the deserialized signatures
const signatures = helios.TxWitnesses.fromCbor(helios.hexToBytes(response)).signatures

tx.addSignatures(signatures)

Note: the bytesToHex and hexToBytes functions are provided by the Helios library as convenient and unambiguous ways to convert a byte-array between string hexadecimal format and raw lists of bytes.

Submitting

After adding the wallet signatures to the transaction, the transaction can be submitted:

// in async context

// returns the hash of the tx
await walletHandle.submitTx(helios.bytesToHex(tx.toCbor()))

Example: PicoSwap

This section walks you through building a minimal marketplace DApp using Helios. The full demo is hosted here.

Only the Helios-specific parts are covered (i.e. not the UI, not the wallet interaction, and not the blockchain queries). This example is intended as an alternate introduction to the Helios API, and shouldn't be seen as an authoritative guide on how to write secure DApps.

We assume here that the library has been imported in the following way:

import * as helios from "helios"

Overview

Main script

The source of the validator script can be placed in a js literal string:

const mainScript = `
spending picoswap

// Note: each input UTxO must contain some lovelace, so the datum price will be a bit higher than the nominal price
// Note: public sales are possible when a buyer isn't specified
 
struct Datum {
    seller: PubKeyHash
    price:  Value              
    buyer:  Option[PubKeyHash]
    nonce:  Int // double satisfaction protection
 
    func seller_signed(self, tx: Tx) -> Bool {
        tx.is_signed_by(self.seller)
    }
 
    func buyer_signed(self, tx: Tx) -> Bool {
        self.buyer.switch{
            None    => true,
            s: Some => tx.is_signed_by(s.some)
        }
    }
 
    func seller_received_money(self, tx: Tx) -> Bool {
        // protect against double satisfaction exploit by datum tagging the output using a nonce
        tx.value_sent_to_datum(self.seller, self.nonce, false) >= self.price
     }
 }
 
func main(datum: Datum, _, ctx: ScriptContext) -> Bool {
    tx: Tx = ctx.tx;
 
    // sellers can do whatever they want with the locked UTxOs
    datum.seller_signed(tx) || (
        // buyers can do whatever they want with the locked UTxOs, as long as the sellers receive their end of the deal
        datum.buyer_signed(tx) && 
        datum.seller_received_money(tx)
    )
}`

We recommend including a Show script or Show contract button in every DApp so users can easily audit the smart contract logic they are interacting with.

Generating datums

We can use the following Helios code to generate datums:

const datumScript = `
const SELLER_BYTES   = # // must be 28 bytes long
const PRICE_LOVELACE = 0
const BUYER_BYTES    = # // must be 0 or 28 bytes long
const NONCE          = 0

const DATUM = Datum{
    seller: PubKeyHash::new(SELLER_BYTES),
    price:  Value::lovelace(PRICE_LOVELACE),
    buyer:  if (BUYER_BYTES.length == 0) {
                Option[PubKeyHash]::None
            } else {
                Option[PubKeyHash]::Some{PubKeyHash::new(BUYER_BYTES)}
            },
    nonce:  NONCE
}`

Before generating a datum with evalParam, we concatenate mainScript with datumScript and change the values of the input parameters:

/**
 * @param {helios.Address} seller
 * @param {bigint} price
 * @returns {helios.UplcData}
 */
function generatePublicSaleDatum(seller, price) {
    // public sale, don't set the buyer bytes
    return helios.Program.new(mainScript + datumScript)
        .changeParam("SELLER_BYTES",   JSON.stringify(seller.pubKeyHash.bytes))
        .changeParam("PRICE_LOVELACE", price.toString())
        .changeParam("NONCE",          (Math.random()*1000000).toString())
        .evalParam("DATUM").data
}

Note: the program.changParam() method takes as a second argument a JSON string, or a UplcValue (i.e. the result of a evalParam call). It doesn't take an arbitrary object however, as that might get confused for the internals of a UplcValue.

Contract helper class

Before describing PicoSwap's smart contract end-points, it is helpful to define a Contract class that can be instantiated for each set of UTxOs locked at the script address having the same datum.

class Contract {
    /**
     * @param {helios.ConstrData} datum - not a helios.Datum instance!
     * @param {helios.UTxO[]} utxos
     */
    constructor(datum, utxos) {
        this.datum = datum
        this.utxos = utxos
    }

    get seller() {
        return new helios.PubKeyHash(this.datum.fields[0].bytes)
    }

    get sellerAddress() {
        // true -> testnet
        return helios.Address.fromPubKeyHash(true, this.seller)
    }

    get price() {
        return helios.Value.fromData(this.datum.fields[1])
    }

    get forSale() {
        return helios.UTxO.sumValue(this.utxos)
    }

    get nonce() {
        return this.datum.fields[3].int
    }
}

Note: ConstrData is one of the 5 child-types of UplcData. The other UplcData child-types are: IntData, ByteArrayData, ListData and MapData.

Grouping the contract UTxOs and extracting the inline datum data is left as an exercise to the reader.

Creating a new sale

A new sale is created by sending funds (the forSale assets) to the contract address with the appropriate datum.

The following function creates a transaction that represents a new sale:

/**
 * @param {helios.Value} forSale
 * @param {bigint} price - in lovelace
 * @returns {Promise<helios.Tx>} - the finalized, but unsigned transaction
 */
async function createNewSaleTx(forSale, price) {
    const uplcProgram = helios.Program.new(mainScript).compile(true)

    const forSaleUtxos = /* code that picks some utxos that cover the 'forSale' value */
    const changeAddress = /* code that picks the changeAddress */

    // create the forSale output that will be locked at the script address
    const output = new helios.TxOutput(
        helios.Address.fromValidatorHash(
            true, // true -> testNet
            uplcProgram.validatorHash
        ),
        forSale,
        helios.Datum.inline(generatePublicSaleDatum(changeAddress, price)) // changeAddress is also the seller address
    )

    // the output might not contain any lovelace, that must be corrected (and the price in the datum must be increased accordingly)
    output.correctLovelace(networkParams, (output) => {
        // increase the price by the min amount of lovelace needed as a deposit
        output.setDatum(
            helios.Datum.inline(
                generatePublicSaleDatum(
                    changeAddress, 
                    price + output.value.lovelace
                )
            )
        );
    })    

    return await ((new helios.Tx())
        .addInputs(forSaleUtxos)
        .addOutput(output)
        .finalize(networkParams, changeAddress)
    )
}

Canceling a sale

A seller can cancel a sale before it is fulfilled.

The following function creates the cancel transaction:

/**
 * @param {Contract} contract - instantiated elsewhere
 * @returns {Promise<helios.Tx>} - finalized but unsigned transaction
 */
async function cancelSaleTx(contract) {
    const uplcProgram = helios.Program.new(mainScript).compile(true)

    const feeUtxos = /* code that picks the utxos with which the tx fee will be paid */
    const changeAddress = /* code that picks the change address */

    // we must add the seller as an explicit signer, some collateral and attach the script
    return await ((new helios.Tx())
        .addInputs(feeUtxos)
        .addInputs(contract.utxos, new helios.IntData(42n)) // dummy redeemer
        // send all the contract utxos back to the seller (i.e. back to the changeAddress)
        .addOutputs(contract.utxos.map(utxo => new helios.TxOutput( 
            changeAddress, utxo.origOutput.value
        )))
        .addSigner(contract.seller)
        .addCollateral(feeUtxos[0]) // assume one of the feeUtxos is big enough to be used as collateral
        .attachScript(uplcProgram)
        .finalize(networkParams, changeAddress)
    )
}

Buying for-sale assets

Anyone can buy the assets locked in the script by sending the price Value to the seller.

The following function creates the buy transaction:

/**
 * @param {Contract} contract - instantiated elsewhere
 * @returns {Promise<helios.Tx>} - finalized but unsigned transaction
 */
async function buyTx(contract) {
    const uplcProgram = helios.Program.new(mainScript).compile(true)

    const paymentUtxos = /* code that picks the utxos used for payment */
    const changeAddress = /* code that picks the change address */
    
    return await ((new helios,Tx())
        .addInputs(paymentUtxos)
        .addInputs(contract.utxos, new helios.IntData(42n)) // dummy redeemer
        .attachScript(uplcProgram)
        .addOutput(new helios.TxOutput( // send 'price' to seller
            contract.sellerAddress,
            contract.price,
            helios.Datum.hashed(new helios.IntData(contract.nonce)) // nonce that protects agains double satisfaction exploit
        ))
        .addOutputs(contract.utxos.map( // send for-sale assets to buyer
            // preserve the number of UTxOs
            utxo => new helios.TxOutput(changeAddress, utxo.value)
        ))
        .addCollateral(/* code that picks collateral UTxOs */)
        .finalize(networkParams, changeAddress)
    )
}

API reference

This section contains a complete reference of all the functions and classes exported by the Helios library. Note that only the class methods that are intended for external use are documented here, even if many more methods can be accessed.

Typescript annotations are used to document types.

Overview

Global constants and variables

Functions

Interfaces

Can only be imported in typescript files.

Classes

Global constants and variables

Constants

VERSION

Current version of the Helios library.

helios.VERSION: string

Variables

Modifiable through the config object.

DEBUG

Currently unused by the library itself. Defaults to false.

helios.config.DEBUG: boolean = false

IS_TESTNET

If true, Addresses are built for testnet by default, otherwise for mainnet. Defaults to true.

helios.config.IS_TESTNET: boolean = true

STRICT_BABBAGE

If true, serializes TxOutput using strictly the Babagge cddl format. Defaults to false.

helios.config.STRICT_BABBAGE: boolean = false

Functions

bytesToHex

Converts a list of bytes into its hexadecimal string representation.

helios.bytesToHex(bytes: number[]): string

bytesToText

Converts utf-8 encoded text from its byte representation to its string representation.

helios.bytesToText(bytes: number[]): string

deserializeUplcBytes

Deserializes a flat encoded UplcProgram.

helios.deserializeUplcBytes(bytes: number[]): helios.UplcProgram

extractScriptPurposeAndName

Quickly extract the script purpose header of a script source, by parsing only the minimally necessary characters. Returns null if the script header is missing or syntactically incorrect.

The first string returned is the script purpose, the second value returned is the script name.

helios.extractScriptPurposeAndName(src: string): ?[string, string]

hexToBytes

Converts a hexadecimal string into a list of bytes.

helios.hexToBytes(hex: string): number[]

highlight

Returns Uint8Array with the same length as the number of chars in the script. Each resulting byte respresents a different syntax category. This approach should faster than a regexp based a approach.

helios.highlight(src: string): Uint8Array

hl

Template string tag function that doens't do anything and just returns the template string as a string. Can be used as a marker of Helios sources so that syntax highlighting can work inside js/ts files.

helios.hl`...`: string

textToBytes

Converts a string into its utf-8 encoded byte representation.

helios.textToBytes(text: string): number[]

Address

Wrapper for Cardano address bytes. An Address consists of three parts internally:

  • Header (1 byte, see CIP 19)
  • Witness hash (28 bytes that represent the PubKeyHash or ValidatorHash)
  • Optional staking credential (0 or 28 bytes)

Constructor

new helios.Address(bytes: []number)

Static methods

fromBech32

Converts a Bech32 string into an Address:

helios.Address.fromBech32(str: string): helios.Address

fromCbor

Deserializes bytes into an Address.

helios.Address.fromCbor(bytes: []number): helios.Address

fromHex

Constructs an Address using a hexadecimal string representation of the address bytes.

helios.Address.fromHex(hex: string): helios.Address

fromHashes

Constructs an Address using either a PubKeyHash (i.e. simple payment address) or ValidatorHash (i.e. script address), in combination with an optional staking hash (StakeKeyHash or StakingValidatorHash).

Testnet addresses have different header bytes. IS_TESTNET is a library-scope variable that can be set globally.

helios.Address.fromPubKeyHash(
    pkh: helios.PubKeyHash | helios.ValidatorHash,
    sh: ?(helios.StakeKeyHash | helios.StakingValidatorHash) = null,
    isTestnet: boolean = IS_TESTNET
): helios.Address

isForTestnet

Returns true if the given Address is a testnet address.

helios.Address.isForTestnet(
    address: helios.Address
): boolean

Getters

pubKeyHash

Returns the underlying PubKeyHash of a simple payment address, or null for a script Address.

address.pubKeyHash: ?helios.PubKeyHash

validatorHash

Returns the underlying ValidatorHash of a script address, or null for a regular payment Address.

address.validatorHash: ?helios.ValidatorHash

stakingHash

Returns the underlying [StakeKeyHash](./stakekeyhash.md) or [StakingValidatorHash](./stakingvalidatorhash.md), or null` for non-staked addresses.

address.stakingHash: ?(helios.StakeKeyHash | helios.StakingValidatorHash)

Methods

toBech32

Turns an Address into its Bech32 representation.

address.toBech32(): string

toCbor

Turns an Address into its CBOR representation.

address.toCbor(): number[]

toHex

Turns a Address into its hexadecimal representation.

address.toHex(): string

Assets

Represents a list of tokens. An Assets instance is used as the second argument of the Value constructor (the first argument is the number of lovelace).

Constructor

The Assets contructor takes one argument: a list of pairs.

The first item of each pair is a MintingPolicyHash, the second item of each pair is another list of pairs of token name bytes and token quantities.

new helios.Assets(assets: [
    helios.MintingPolicyHash,
    [number[], bigint][]
][])

Getters

mintingPolicies

Returns a list of all the minting policies (as a list of MintingPolicyHashes).

assets.mintingPolicies: helios.MintingPolicyHash[]

BlockfrostV0

Blockfrost specific implementation of Network.

WiP.

Constructor

Constructs a BlockfrostV0 using the network name (preview, preprod or mainnet) and your Blockfrost project id.

const network = new helios.BlockfrostV0(networkName: string, projectId: string)

Static methods

resolve

Connect the same network a Wallet is connected to (preview, preprod or mainnet). Throws an error if a Blockfrost project id is missing for that specific network.

const network = helios.BlockforstV0.resolve(
    wallet: helios.Wallet,
    projectIds: {
        preview?: string,
        preprod?: string,
        mainnet?: string
    }
)

Methods

getUtxos

Gets a complete list of UTxOs at a given address.

network.getUtxos(address: helios.Address): Promise<helios.UTxO[]>

submitTx

Submits a transaction to the blockchain. Returns the TxId.

network.submitTx(tx: helios.Tx): Promise<helios.TxId>

ByteArrayData

One of Plutus-Core's 5 builtin data types, representing a list of bytes. Parent class is UplcData.

Constructor

new helios.ByteArrayData(bytes: []number)

Static methods

fromCbor

Decodes CBOR bytes representing a ByteArrayData instance. Mutates the bytes argument.

helios.ByteArrayData.fromCbor(bytes: []number): helios.ByteArrayData

fromString

Encodes a utf-8 string as a ByteArrayData instance.

helios.ByteArrayData.fromString(str: string): helios.ByteArrayData

Getters

bytes

Returns the underlying list of bytes.

byte_array_data.bytes: number[]

Methods

toCbor

Encodes a ByteArrayData instance using CBOR.

byte_array_data.toCbor(): number[]

toHex

Returns the hexadecimal string representation of the underlying bytes.

byte_array_data.toHex(): string

toSchemaJson

Returns the JSON representation of a ByteArrayData instance. Needed for interacting with external tools like cardano-cli and Lucid.

byte_array_data.toSchemaJson(): string

CborData

Base class of each CBOR (de)serializeable type. CborData also contains many static helper methods which can be used to decode/encode CBOR data.

Static methods

decodeBool

Decodes a CBOR encoded boolean. Input list is mutated. Throws an error if the next element in bytes isn't a boolean.

helios.CborData.decodeBool(bytes: number[]): boolean

decodeBytes

Unwraps a CBOR encoded list of bytes. Mutates bytes and moves to the following element.

helios.CborData.decodeBytes(bytes: number[]): number[]

decodeInteger

Decode a CBOR encoded bigint integer. Mutates bytes and moves to the following element.

helios.CborData.decodeInteger(bytes: number[]): bigint

decodeList

Decodes a CBOR encoded list. A decoder function is called with the bytes of every contained item (nothing is returning directly). Mutates bytes and moves on to element following the list.

helios.CborData.decodeList(
    bytes: number[], 
    decoder: (number[]) => void
): void

decodeMap

Decodes a CBOR encoded map. Calls a decoder function for each key-value pair (nothing is returned directly).

The decoder function is responsible for separating the key from the value, which are simply stored as consecutive CBOR elements.

helios.CborData.decodeMap(
    bytes: number[],
    decoder: (number[]) => void
): void

decodeNull

Checks if next element in bytes is a null. Throws an error if it isn't. Mutates bytes by moving to the following element.

helios.CborData.decodeNull(bytes: number[]): void

decodeObject

Decodes a CBOR encoded object. For each field a decoder is called which takes the field index and the field bytes as arguments.

helios.CborData.decodeObject(
    bytes: number[],
    decoder: (number, number[]) => void
): void

encodeBool

Encode a boolean into its CBOR representation.

helios.CborData.encodeBool(b: boolean): number[]

encodeBytes

Wraps a list of bytes using CBOR. Optionally splits the bytes in chunks.

helios.CborData.encodeBytes(
    bytes: number[],
    splitInChunks: boolean = false
): number[]

encodeDefList

Encodes a list of CBOR encodeable items using CBOR definite length encoding (i.e. header bytes of the element represent the length of the list).

Each item is CborData child instances with the toCbor method defined, or an already encoded list of CBOR bytes.

helios.CborData.encodeDefList(
    list: helios.CborData[] | number[][]
): number[]

encodeIndefList

Encodes a list of CBOR encodeable items using CBOR indefinite length encoding.

Each item is eiter a CborData child instance with the toCbor method defined, or an already encoded list of CBOR bytes.

helios.encodeIndefList(
    list: helios.CborData[] | number[][]
): number[]

encodeInteger

Encodes a bigint integer using CBOR.

helios.CborData.encodeInteger(x: bigint): number[]

encodeMap

Encodes a list of key-value pairs. Each key and each value is either a CborData child instance with the toCbor method defined, or an already encoded list of CBOR bytes.

helios.CborData.encodeMap(
    pairs: [
        helios.CborData | number[],
        helios.CborData | number[]
    ][]
): number[]

encodeNull

Encode a null into its CBOR representation.

helios.CborData.encodNull(): number[]

encodeObject

Encodes an object with optional fields. A CBOR object element is simply a map element with integer keys representing the field index.

helios.encodeObject(
    object: Map<
        number,
        helios.CborData | number[]
    >
): number[]

Cip30Handle

Convenience type for browser plugin wallets supporting the CIP 30 DApp connector standard (eg. Eternl, Nami).

interface Cip30Handle {
    getNetworkId(): Promise<number>,
    getUsedAddresses(): Promise<string[]>,
    getUnusedAddresses(): Promise<string[]>,
    getUtxos(): Promise<string[]>,
    signTx(txHex: string, partialSign: boolean): Promise<string>,
    submitTx(txHex: string): Promise<string>
}

This is useful in typescript projects to avoid type errors when accessing the handles in window.cardano.

// refer to this file in the 'typeRoots' list in tsconfig.json

type Cip30SimpleHandle = {
    name: string,
    icon: string,
    enable(): Promise<helios.Cip30Handle>,
    isEnabled(): boolean
}

declare global {
  interface Window {
    cardano: {
        [walletName: string]: Cip30SimpleHandle
    };  
  }
}

Cip30Wallet

Implementation of Wallet that lets you connect to a browser plugin wallet.

Constructor

Constructs Cip30Wallet using the Cip30Handle which is available in the browser window.cardano context.

const handle: helios.Cip30Handle = await window.cardano.eternl.enable()

const wallet = new helios.Cip30Wallet(handle)

Getters

usedAddresses

Gets a list of addresses which already contain UTxOs.

wallet.usedAddresses: Promise<helios.Address[]>

unusedAddresses

Gets a list of unique unused addresses which can be used to UTxOs to.

wallet.unusedAddresses: Promise<helios.Address[]>

utxos

Gets the complete list of UTxOs sitting at the addresses owned by the wallet.

wallet.utxos: Promise<helios.UTxO[]>

Methods

isMainnet

Returns true if the wallet is connected to the mainnet.

wallet.isMainnet(): Promise<boolean>

signTx

Signs a transaction, returning a list of signatures needed for submitting a valid transaction.

wallet.signTx(tx: helios.Tx): Promise<helios.Signature[]>

submitTx

Submits a transaction to the blockchain. Returns the TxId.

wallet.submitTx(tx: helios.Tx): Promise<helios.TxId>

CoinSelection

Static class with common coin selection algorithms.

Static methods

selectLargestFirst

Selects UTxOs from a list by iterating through the tokens in the given Value and picking the UTxOs containing the largest corresponding amount first.

Returns two lists. The first list contains the selected UTxOs, the second list contains the remaining UTxOs.

helios.CoinSelection.selectLargestFirst(
    utxos: helios.UTxO[],
    amount: helios.Value
)

selectSmallestFirst

Selects UTxOs from a list by iterating through the tokens in the given Value and picking the UTxOs containing the smallest corresponding amount first. This method can be used to eliminate dust UTxOs from a wallet.

Returns two lists. The first list contains the selected UTxOs, the second list contains the remaining UTxOs.

helios.CoinSelection.selectSmallestFirst(
    utxos: helios.UTxO[],
    amount: helios.Value
)

ConstrData

One of the 5 Plutus-Core builtin data classes. Parent class is UplcData.

Represents a tag index and a list of data fields. Each field is also a UplcData child instance.

Constructor

new helios.ConstrData(
    index: number,
    fields: helios.UplcData[]
)

Static methods

fromCbor

Decoded a CBOR encoded ConstrData instance. Mutates bytes and shifts it to the following element.

helios.ConstrData.fromCbor(bytes: number[]): helios.ConstrData

Getters

index

Returns the ConstrData tag.

constr_data.index: number

fields

Returns the ConstrData fields.

constr_data.fields: helios.UplcData[]

Methods

toCbor

Encodes a ConstrData instance as CBOR bytes.

constr_data.toCbor(): number[]

toSchemaJson

Returns the schema JSON needed to interact with external tools like cardano-cli and Lucid.

constr_data.toSchemaJson(): string

Datum

Represents either an inline datum, or a hashed datum.

Static methods

fromCbor

Decodes a CBOR encoded datum. Can be inline or hashed.

Mutates bytes and shifts it to the following element.

helios.Datum.fromCbor(bytes: number[]): helios.Datum

hashed

Constructs a HashedDatum. The input data is hashed internally.

helios.Datum.hashed(
    data: helios.UplcDataValue | helios.UplcData
): helios.HashedDatum

inline

Constructs an InlineDatum.

helios.Datum.inline(
    data: helios.UplcDataValue | helios.UplcData
): helios.InlineDatum

Getters

data

Return the underlying data, or null if not available.

datum.data: ?helios.UplcData

hash

Get the DatumHash.

datum.hash: helios.DatumHash

Methods

isHashed

Returns true if this is a hashed datum.

datum.isHashed(): boolean

isInline

Returns true if this is an inline datum.

datum.isInline(): boolean

DatumHash

Represents a blake2b-256 hash of datum data.

Static methods

fromCbor

Deserialize a CBOR encoded DatumHash.

Mutates bytes and shifts it to the next CBOR element.

helios.DatumHash.fromCbor(bytes: number[]): helios.DatumHash

fromHex

Construct a DatumHash from the hexadecimal string representation of the underlying bytes.

helios.DatumHash.fromHex(hex: string): helios.DatumHash

Getters

bytes

Get the underlying bytes.

datum_hash.bytes: number[]

hex

Returns the hexadecimal representation of the underlying bytes.

datum_hash.hex: string

FuzzyTest

Helper class for performing fuzzy property-based tests of Helios scripts.

Constructor

new helios.FuzzyTest(
    seed: number = 0,
    runsPerTest: number = 100,
    simplify: boolean = false
)

The simplify argument specifies whether optimized versions of the Helios sources should also be tested.

Methods

ascii

Returns a generator for ByteArrayData instances representing ascii strings.

fuzzy_test.ascii(
    minLength: number = 0,
    maxLength: number = 64
): (() => helios.ByteArrayData)

bool

Returns a generator for primitive boolean instances.

fuzzy_test.bool(): (() => helios.ConstrData)

bytes

Returns a generator for ByteArrayData.

fuzzy_test.bytes(
    minLength: number = 0,
    maxLength: number = 64
): (() => helios.ByteArrayData)

int

Returns a generator for IntData.

fuzzy_test.int(
    min: number = -10000000,
    max: number =  10000000
): (() => helios.IntData)

list

Returns a generator for ListData, where every item is generated by the given itemGenerator.

fuzzy_test.list(
    itemGenerator: () => helios.UplcData,
    minLength: number = 0,
    maxLength: number = 10
): (() => helios.ListData)

map

Returns a generator for MapData, where every key and value is generated by the given keyGenerator and valueGenerator respectively.

fuzzy_test.map(
    keyGenerator: () => helios.UplcData,
    valueGenerator: () => helios.UplcData,
    minLength: number = 0,
    maxLength: number = 10
): (() => helios.MapData)

option

Returns a generator for ConstrData instances representing random options. The probability of none occurences can be configured.

fuzzy_test.option(
    someGenerator: () => helios.UplcData,
    noneProbability: number = 0.5
): (() => helios.ConstrData)

string

Returns a generator for ByteArrayData instances representing utf-8 strings.

fuzzy_test.string(
    minLength: number = 0,
    maxLength: number = 64
): (() => helios.ByteArrayData)

test

Perform a fuzzy/property-based test-run of a Helios source. One value generator must be specified per argument of main.

Throws an error if the propTest fails.

The propTest can simply return a boolean, or can return an object with boolean values, and if any of these booleans is false the propTest fails (the keys can be used to provide extra information).

fuzzy_test.test(
    argGens: (() => helios.UplcData)[],
    src: string,
    propTest: (
        args: helios.UplcValue[], 
        res: helios.UplcValue | helios.UserError
    ) => (boolean | Object.<string, boolean>)
): Promise<void>

HeliosData

Abstract parent class of Helios API types that can be used directly when setting parameters:

Methods

toSchemaJson

Returns a JSON-string in the schema needed for interaction with non-Helios tools.

helios_data.toSchemaJson(): string

IntData

One of the 5 Plutus-Core builtin data classes. Parent class is UplcData.

Represents an unbounded integer (bigint).

Constructor

new helios.IntData(value: bigint)

Static methods

fromCbor

Deserialize a CBOR encoded IntData. Mutates bytes and shifts it to the following element.

helios.IntData.fromCbor(bytes: number[]): IntData

Getters

value

Get the underlying bigint value.

int_data.value: bigint

Methods

toCbor

Encodes a IntData instance as CBOR bytes.

int_data.toCbor(): number[]

toSchemaJson

Returns the schema JSON needed to interact with external tools like cardano-cli and Lucid.

int_data.toSchemaJson(): string

ListData

One of the 5 Plutus-Core builtin data classes. Parent class is UplcData.

Represents a list of other UplcData instances.

Constructor

new helios.ListData(list: UplcData[])

Static methods

fromCbor

Deserialize a CBOR encoded ListData instance. Mutates bytes and shifts it to the following element.

helios.ListData.fromCbor(bytes: number[]): ListData

Getters

list

Get the underlying UplcData list.

list_data.list: UplcData[]

Methods

toCbor

Encodes a ListData instance as CBOR bytes.

list_data.toCbor(): number[]

toSchemaJson

Returns the schema JSON needed to interact with external tools like cardano-cli and Lucid.

list_data.toSchemaJson(): string

MapData

One of the 5 Plutus-Core builtin data classes. Parent class is UplcData.

Represents a list of pairs of other UplcData instances.

Constructor

new helios.MapData(pairs: [UplcData, UplcData][])

Static methods

fromCbor

Deserialize a CBOR encoded MapData instance. Mutates bytes and shifts it to the following element.

helios.MapData.fromCbor(bytes: number[]): MapData

Getters

map

Get the underlying list of UplcData pairs.

map_data.map: [UplcData, UplcData][]

Methods

toCbor

Encodes a MapData instance as CBOR bytes.

map_data.toCbor(): number[]

toSchemaJson

Returns the schema JSON needed to interact with external tools like cardano-cli and Lucid.

map_data.toSchemaJson(): string

MintingPolicyHash

Represents a blake2b-224 hash of a minting policy script (first encoded as a CBOR byte-array and prepended by a script version byte).

Static methods

fromCbor

Deserialize a CBOR encoded MintingPolicyHash.

Mutates bytes and shifts it to the next CBOR element.

helios.MintingPolicyHash.fromCbor(bytes: number[]): helios.MintingPolicyHash

fromHex

Construct a MintingPolicyHash from the hexadecimal string representation of the underlying bytes.

helios.MintingPolicyHash.fromHex(hex: string): helios.MintingPolicyHash

Getters

bytes

Get the underlying bytes.

minting_policy_hash.bytes: number[]

hex

Returns the hexadecimal representation of the underlying bytes.

minting_policy_hash.hex: string

Network

Blockchain query interface. Notably implemented by BlockfrostV0 and NetworkEmulator.

Methods

getUtxos

Gets a complete list of UTxOs at a given address.

network.getUtxs(address: helios.Address): Promise<helios.UTxO[]>

submitTx

Submits a transaction to the blockchain. Returns the TxId.

network.submitTx(tx: helios.Tx): Promise<helios.TxId>

NetworkEmulator

A simple emulated Network. This can be used to do integration tests of whole DApps. Staking is not yet supported.

Constructor

Instantiates a NetworkEmulator at slot 0. An optional seed number can be specified, from which all emulated randomness is derived.

const network = new helios.NetworkEmulator(seed: number = 0)

Methods

createWallet

Creates a new WalletEmulator and populates it with a given lovelace quantity and assets (these are taken from special genesis transactions).

network.createWallet(
    lovelace: bigint,
    assets: helios.Assets
): helios.WalletEmulator

getUtxos

Gets a complete list of UTxOs at a given address.

network.getUtxos(address: helios.Address): Promise<helios.UTxO[]>

submitTx

Validates and submits a transaction to the emulated mempool. Any input UTxOs are immediately marked as spent.

network.submitTx(tx: helios.Tx): Promise<helios.TxId>

tick

Mints a block with the current emulated mempool, and advances the head slot by nSlots.

network.tick(nSlots: bigint)

NetworkParams

Wrapper for the raw JSON containing all the current network parameters.

NetworkParams is needed to be able to calculate script budgets and perform transaction building checks.

The raw JSON can be downloaded from the following CDN locations:

These JSONs are updated daily.

Constructor

// in an async context

const networkParams = new helios.NetworkParams(
    await fetch("https://d1t0d7c2nekuk0.cloudfront.net/preview.json")
        .then(response => response.json())
)

Methods

slotToTime

Calculates the time (in milliseconds in 01/01/1970) associated with a given slot number.

networkParams.slotToTime(slot: bigint): bigint

timeToSlot

Calculates the slot number associated with a given time. Time is specified as milliseconds since 01/01/1970.

networkParams.timeToSlot(time: bigint): bigint

Program

Represents a Helios program containing a main function.

This is the principal API class with which users of the library interact.

Constructor

The constructor isn't intended for direct use. The new static method should be used instead:

helios.Program.new(
    mainSrc: string, 
    moduleSrcs: string[] = []
): helios.Program

The 1st argument here is the source of the entrypoint of the program. The 2nd argument here is optional, and is a list of the sources of modules that can be import by the entrypoint source.

Getters

name

Get the name of the script (contained in the header of the script).

program.name: string

paramTypes

Returns a mapping of top-level const names to const types.

program.paramTypes: Object.<string, helios.Type>

parameters

Returns an object containing all the evaluated parameters.

program.parameters: {[paramName: string]: helios.HeliosData}

HeliosData is the abstract parent class of many Helios API types that have Helios language equivalents.

types

Returns an object containing Javascript contructors for the user-defined types in the main script (including those imported into the main script).

program.types: {[typeName: string]: {new(...any) => helios.HeliosData}}

Instantiating these constructors creates objects with HeliosData as a parent type.

Setters

parameters

Parameters can be set using the parameters setter. Parameters are const statements that are visible in the main Helios script. In many cases a Javascript value can be used directly (i.e. JSON-like).

program.parameters = {MY_PARAM: my_param, ...} as {[name: string]: helios.HeliosData | any}

Primitive Javascript values can also be used as a rhs when setting parameters like this. Helios will intelligently convert these in the necessary HeliosData instances.

Methods

compile

Compiles a Helios program, with optional optimization. Returns a UplcProgram instance.

program.compile(simplify: boolean = false): helios.UplcProgram

evalParam

Eval the rhs of a const statement, and return the result as a UplcValue.

program.evalParam(paramName: string): helios.UplcValue

PubKeyHash

Represents a Blake2-224 hash of public key.

A PubKeyHash is used as the first part of a regular payment Address.

Static methods

fromCbor

Deserialize a CBOR encoded PubKeyHash.

Mutates bytes and shifts it to the next CBOR element.

helios.PubKeyHash.fromCbor(bytes: number[]): helios.PubKeyHash

fromHex

Construct a PubKeyHash from its hexadecimal string representation.

helios.PubKeyHash.fromHex(hex: string): helios.PubKeyHash

Getters

bytes

Get the underlying bytes.

pubkey_hash.bytes: number[]

hex

Returns the hexadecimal representation.

pubkey_hash.hex: string

Signature

Represents a Ed25519 signature associated with a given public key.

Transactions must be signed by the owners of the public keys of the input UTxOs.

Constructor

new helios.Signature(
    pubKey: number[],
    signatureBytes: number[]
)

Static methods

fromCbor

Deserialize a CBOR encoded Signature instance.

helios.Signature.fromCbor(bytes: number{}): helios.Signature

Methods

toCbor

Encodes a Signature instance using CBOR.

signature.toCbor(): number[]

verify

Verify that the signature corresponds with the given message bytes.

Throws an error if the signature is wrong.

signature.verify(messageBytes: number[]): void

StakeAddress

Wrapper for Cardano stake address bytes. An StakeAddress consists of two parts internally:

Stake addresses are used to query the assets held by given staking credentials.

Constructor

new helios.StakeAddress(bytes: []number)

Static methods

fromAddress

Convert a regular Address into a StakeAddress. Throws an error if the Address doesn't have a staking credential.

helios.StakeAddress.fromAddress(address: helios.Address): helios.StakeAddress

fromCbor

Deserializes bytes into an StakeAddress.

helios.StakeAddress.fromCbor(bytes: []number): helios.StakeAddress

fromBech32

Converts a Bech32 string into an StakeAddress:

helios.StakeAddress.fromBech32(str: string): helios.StakeAddress

fromHash

Converts a StakeKeyHash or StakingValidatorHash into StakeAddress.

helios.StakeAddress.fromHash(
    isTestnet: boolean,
    hash: helios.StakeKeyHash | helios.StakingValidatorHash
)

Note: bech32 encoded stake addresses have a "stake" or "stake_test" prefix.

fromHex

Constructs a StakeAddress using a hexadecimal string representation of the address bytes.

helios.StakeAddress.fromHex(hex: string): helios.StakeAddress

isForTestnet

Returns true if the given StakeAddress is a testnet address.

helios.StakeAddress.isForTestnet(
    stake_address: helios.StakeAddress
): boolean

Getters

stakingHash

Returns the underlying StakeKeyHash](./stakekeyhash.md) or [StakingValidatorHash`.

stake_address.stakingHash: (helios.StakeKeyHash | helios.StakingValidatorHash)

Methods

toBech32

Turns a StakeAddress into its Bech32 representation.

stake_address.toBech32(): string

toCbor

Turns a StakeAddress into its CBOR representation.

stake_address.toCbor(): number[]

toHex

Turns a StakeAddress into its hexadecimal representation.

stake_address.toHex(): string

StakeKeyHash

Represents a blake2b-224 hash of staking key.

A StakeKeyHash can be used as the second part of a regular payment Address, or to construct a StakeAddress.

Static methods

fromCbor

Deserialize a CBOR encoded StakeKeyHash.

Mutates bytes and shifts it to the next CBOR element.

helios.StakeKeyHash.fromCbor(bytes: number[]): helios.StakeKeyHash

fromHex

Construct a StakeKeyHash from its hexadecimal string representation.

helios.StakeKeyHash.fromHex(hex: string): helios.StakeKeyHash

Getters

bytes

Get the underlying bytes.

stakekey_hash.bytes: number[]

hex

Returns the hexadecimal representation.

stakekey_hash.hex: string

StakingValidatorHash

Represents a blake2b-224 hash of a staking script (first encoded as a CBOR byte-array and prepended by a script version byte).

Static methods

fromCbor

Deserialize a CBOR encoded StakingValidatorHash.

Mutates bytes and shifts it to the next CBOR element.

helios.StakingValidatorHash.fromCbor(bytes: number[]): helios.StakingValidatorHash

fromHex

Construct a StakingValidatorHash from the hexadecimal string representation of the underlying bytes.

helios.StakingValidatorHash.fromHex(hex: string): helios.StakingValidatorHash

Getters

bytes

Get the underlying bytes.

staking_validator_hash.bytes: number[]

hex

Returns the hexadecimal representation of the underlying bytes.

staking_validator_hash.hex: string

Tx

Represents a cardano transaction. Can also be used a transaction builder.

Constructor

Init a new transaction builder.

new helios.Tx()

Static methods

fromCbor

Deserialize a CBOR encoded Cardano transaction.

helios.Tx.fromCbor(bytes: number[]): helios.Tx

Methods

addCollateral

Add a UTxO instance as collateral to the transaction being built. Usually adding only one collateral input is enough. The number of collateral inputs must be greater than 0 if script witnesses are used in the transaction, and must be less than the limit defined in the NetworkParams.

Mutates the transaction. Only available when building the transaction. Returns the transaction instance so build methods can be chained.

tx.addCollateral(input: helios.TxInput): helios.Tx

addInput

Add a UTxO instance as an input to the transaction being built. Throws an error if the UTxO is locked at a script address but a redeemer isn't specified.

Mutates the transaction. Only available when building the transaction. Returns the transaction instance so build methods can be chained.

tx.addInput(
    input: helios.UTxO,
    redeemer: ?(helios.UplcData | helios.UplcDataValue) = null
): helios.Tx

addInputs

Add multiple UTxO instances as inputs to the transaction being built. Throws an error if the UTxOs are locked at a script address but a redeemer isn't specified.

Mutates the transaction. Only available when building the transaction. Returns the transaction instance so build methods can be chained.

tx.addInputs(
    inputs: helios.UTxO[],
    redeemer: ?(helios.UplcData | helios.UplcDataValue) = null
): helios.Tx

addMetadata

Add metadata to a transaction. Metadata can be used to store data on-chain, but can't be consumed by validator scripts. Metadata can for example be used for CIP 25.

tx.addMetadata(
    tag: number, // whole number
    metadata: Metadata
)

The Metadata type is an alias for the following JSON schema:

@typedef {
  string |
  number | // whole numbers only
  Metadata[] | 
  {map: [Metadata, Metadata][]} // a map is implemented as a list of pairs because order needs to be respected
} Metadata

addOutput

Add a TxOutput instance to the transaction being built.

Mutates the transaction. Only available when building the transaction. Returns the transaction instance so build methods can be chained.

tx.addOutput(output: helios.TxOutput): helios.Tx

addOutputs

Add multiple TxOutput instances at once.

Mutates the transaction. Only available when building the transaction. Returns the transaction instance so build methods can be chained.

tx.addOutputs(outputs: helios.TxOutput[]): helios.Tx

addRefInput

Add a TxRefInput instance as a reference input to the transaction being built. An associated reference script, as a UplcProgram instance, must also be included in the transaction at this point (so the that the execution budget can be calculated correctly).

Mutates the transaction. Only available when building the transaction. Returns the transaction instance so build methods can be chained.

tx.addRefInput(
    input: helios.TxRefInput
    refScript: ?helios.UplcProgram = null
): helios.Tx

addRefInputs

Add multiple TxRefInput instances as reference inputs to the transaction being built.

Mutates the transaction. Only available when building the transaction. Returns the transaction instance so build methods can be chained.

tx.addRefInputs(inputs: helios.TxRefInput[]): helios.Tx

addSignature

Add a signature created by a wallet. Only available after the transaction has been finalized. Optionally verify that the signature is correct.

tx.addSignature(
    signature: helios.Signature,
    verify: boolean = true
): helios.Tx

addSignatures

Add multiple signatures at once. Only available after the transaction has been finalized. Optionally verify correctness (could be slow for many signatures).

tx.addSignatures(
    signatures: helios.Signature[],
    verify: boolean = true
): helios.Tx

addSigner

Add a signatory PubKeyHash to the transaction being built. The added entry becomes available in the tx.signatories field in the Helios script.

Mutates the transaction. Only available when building the transaction. Returns the transaction instance so build methods can be chained.

tx.addSigner(hash: helios.PubKeyHash): helios.Tx

attachScript

Attach a script witness to the transaction being built. The script witness is a UplcProgram instance and can be created by compiling a Helios Program.

Throws an error if script has already been added. Throws an error if the script isn't used upon finalization.

Mutates the transaction. Only available when building the transaction. Returns the transaction instance so build methods can be chained.

tx.attachScript(script: helios.UplcProgram): helios.Tx

finalize

Executes all the attached scripts with appropriate redeemers and calculates execution budgets. Balances the transaction, and optionally uses some spare UTxOs if the current inputs don't contain enough lovelace to cover the fees and min output deposits.

Inputs, minted assets, and withdrawals are sorted.

tx.finalize(
    networkParams: helios.NetworkParams,
    changeAddress: helios.Address,
    spareUtxos:    helios.UTxO[]
): Promise<Tx>

mintTokens

Mint a list of tokens associated with a given MintingPolicyHash. Throws an error if the given MintingPolicyHash was already used in a previous call to mintTokens. The token names can either by a list of bytes or a hexadecimal string.

Mutates the transaction. Only available when building the transaction. Returns the transaction instance so build methods can be chained.

tx.mintTokens(
    mph: helios.MintingPolicyHash,
    tokens: [number[] | string, bigint][],
    redeemer: helios.UplcData | helios.UplcDataValue
): helios.Tx

toCbor

Serialize a transaction.

tx.toCbor(): number[]

validFrom

Set the start of the valid time range.

Mutates the transaction. Only available when building the transaction. Returns the transaction instance so build methods can be chained.

tx.validFrom(time: Date): helios.Tx

validTo

Set the end of the valid time range. Mutates the transaction. Only available when building a transaction.

Returns the transaction instance so build methods can be chained.

tx.validTo(time: Date): helios.Tx

TxId

Represents the hash of a transaction. This is used to identify an UTxO (along with the index of the UTxO in the list of UTxOs created by the transaction).

Static methods

fromCbor

Deserialize a CBOR encoded TxId.

Shift bytes to the next CBOR element.

helios.TxId.fromCbor(bytes: number[]): helios.TxId

fromHex

Construct a TxId from the hexadecimal string representation of the underlying bytes.

helios.TxId.fromHex(hex: string): helios.TxId

Getters

bytes

Get the underlying bytes.

tx_id.bytes: number[]

hex

Returns the hexadecimal representation of the underlying bytes.

tx_id.hex: string

TxOutput

Represents a transaction output that is used when building a transaction.

Constructor

Constructs a TxOutput instance using an Address, a Value, an optional Datum, and optional UplcProgram reference script.

new helios.TxOutput(
    address:   helios.Address,
    value:     helios.Value,
    datum:     ?helios.Datum = null,
    refScript: ?helios.UplcProgram = null
)

Static methods

fromCbor

Deserialize a CBOR encoded TxOutput.

Shift bytes to the next CBOR element.

helios.TxOutput.fromCbor(bytes: number[]): helios.TxOutput

Getters

address

Get the Address to which the TxOutput will be sent.

tx_output.address: helios.Address

datum

Get the optional Datum associated with the TxOutput.

tx_output.datum: helios.Datum

value

Get the Value contained in the TxOutput.

tx_output.value: helios.Value

Methods

correctLovelace

Makes sure the TxOutput contains the minimum quantity of lovelace. The network requires this to avoid the creation of unusable dust UTxOs.

Optionally an update function can be specified that allows mutating the datum of the TxOutput to account for an increase of the lovelace quantity contained in the value.

tx_output.correctLovelace(
    networkParams: helios.NetworkParams,
    updater: ?((self: helios.TxOutput) => void)
): void

setDatum

Mutates the TxOutput datum.

tx_output.setDatum(datum: helios.Datum): void

toCbor

Serialize a TxOutput using CBOR.

tx_output.toCbor(): number[]

TxRefInput

A reference input (similar to UTxO, but without information about original transaction output).

Needed for tx.addRefInput() and tx.addRefInputs().

Constructor

A TxRefInput instance is constructed using the TxId where the UTxO was created, and the index of that UTxO in the list of UTxOs created by that transactions.

new helios.TxRefInput(
    txId: TxId,
    utxoIdx: bigint
)

TxWitnesses

Represents the pubkey signatures, and datums/redeemers/scripts that are witnessing a transaction.

The CBOR representation of TxWitnesses is returned by a wallet when signing a transaction.

Static methods

fromCbor

Deserialize a CBOR encoded TxWitnesses instance.

Shifts bytes to the next CBOR element.

helios.TxWitnesses.fromCbor(bytes: number[]): helios.TxWitnesses

Getters

signatures

Gets the list of Signature instances created by a wallet.

tx_witnesses.signatures: helios.Signature[]

UplcBool

API equivalent of Bool.

Static methods

new

Constructs a UplcBool instance.

helios.UplcBool.new(value: boolean): helios.UplcBool

UplcByteArray

Primitive equivalent of ByteArrayData. Not needed to interact with Helios smart contracts, but can be used to interact with smart contracts written in other languages (eg. Plutarch).

Static methods

new

Constructs a UplcByteArray instance.

helios.UplcByteArray.new(value: number[]): helios.UplcByteArray

UplcData

Parent class of:

Static methods

fromCbor

Deserializes a CBOR encoded UplcData child instance.

Shift bytes to the next CBOR element.

helios.UplcData.fromCbor(bytes: number[]): 
    helios.ByteArrayData |
    helios.ConstrData |
    helios.IntData |
    helios.ListData |
    helios.MapData

UplcDataValue

Represents a child instance of UplcValue, wrapping a UplcData instance.

Everything except a Helios Bool value evaluates to a UplcDataValue.

Getters

data

Get the underlying UplcData.

uplc_data_value.data: helios.UplcData

UplcInt

Primitive equivalent of IntData. Not needed to interact with Helios smart contracts, but can be used to interact with smart contracts written in other languages (eg. Plutarch).

Static methods

new

Constructs a UplcInt instance. value must be a whole number.

helios.UplcInt.new(value: number | bigint): helios.UplcInt

UplcPair

Primitive pair value. Not needed to interact with Helios smart contracts, but can be used to interact with smart contracts written in other languages (eg. Plutarch).

Static methods

new

Constructs a UplcPair instance using two UplcValue instances.

helios.UplcPair.new(
    first: helios.UplcValue, 
    second: helios.UplcValue
): helios.UplcPair

UplcProgram

Result of program.compile(). Contains the Untyped Plutus-Core AST, along with a code-mapping to the original source.

Static methods

fromCbor

Deserialize a UplcProgram from bytes.

helios.UplcProgram.fromCbor(bytes: number[]): helios.UplcProgram

Getters

validatorHash

Returns the ValidatorHash of the script. Throws an error if this isn't a spending validator script.

uplcProgram.validatorHash: helios.ValidatorHash

mintingPolicyHash

Returns the MintingPolicyHash of the script. Throws an error if this isn't a minting policy.

uplcProgram.mintingPolicyHash: helios.MintingPolicyHash

stakingValidatorHash

Returns the StakingValidatorHash of the script. Throws an error if this isn't a staking validator script.

uplcProgram.stakingValidatorHash: helios.StakingValidatorHash

Methods

apply

Wrap the top-level term with consecutive UplcCall (not exported) terms.

Returns a new UplcProgram instance, leaving the original untouched.

uplcProgram.apply(args: helios.UplcValue[]): helios.UplcProgram

profile

Runs and profiles a UplcProgram. Needs the NetworkParams in order to calculate the execution budget.

uplcProgram.profile(
    args: helios.UplcValue[],
    networkParams: helios.NetworkParams
): Promise<{
    mem: bigint,
    cpu: bigint,
    size: number,
    res: helios.UplcValue | helios.UserError
}>

runWithPrint

Run a UplcProgram. The printed messages are part of the return value.

uplcProgram.runWithPrint(args: helios.UplcValue[]): 
    Promise<[helios.UplcValue | helios.UserError, string[]]>

serialize

Serialize a UplcProgram using flat encoding. Returns the JSON representation of the program (needed by cardano-cli).

uplcProgram.serialize(): string

serializeBytes

Serialize a UplcProgram using flat encoding. Returns a list of bytes.

uplcProgram.serializeBytes(): number[]

toCbor

Serialize a UplcProgram using CBOR (wraps the flat encoded program).

uplcProgram.toCbor(): number[]

UplcString

Primitive string value. Not needed to interact with Helios smart contracts, but can be used to interact with smart contracts written in other languages (eg. Plutarch).

Static methods

new

Constructs a UplcString instance.

helios.UplcString.new(value: string): helios.UplcString

UplcUnit

Primitive unit value. Not needed to interact with Helios smart contracts, but can be used to interact with smart contracts written in other languages (eg. Plutarch).

Static methods

new

Constructs a UplcUnit instance.

helios.UplcUnit.new(): helios.UplcUnit

UplcValue

Parent class of every internal Helios value:

UserError

Represents an error thrown when there is a mistake in a Helios source.

Getters

message

Gets the error message.

user_error.message: string

UTxO

Unspent Transaction Output that can be used as an input to when builing a transaction. UTxO instances are also returned when interfacing with Wallet.

Constructor

new helios.UTxO(
    txId: helios.TxId,
    utxoIdx: bigint,
    origOutput: helios.TxOutput
)

ValidatorHash

Represents a blake2b-224 hash of a spending validator script (first encoded as a CBOR byte-array and prepended by a script version byte).

Static methods

fromCbor

Deserialize a CBOR encoded ValidatorHash.

Mutates bytes and shifts it to the next CBOR element.

helios.ValidatorHash.fromCbor(bytes: number[]): helios.ValidatorHash

fromHex

Construct a ValidatorHash from the hexadecimal string representation of the underlying bytes.

helios.ValidatorHash.fromHex(hex: string): helios.ValidatorHash

Getters

bytes

Get the underlying bytes.

validator_hash.bytes: number[]

hex

Returns the hexadecimal representation of the underlying bytes.

validator_hash.hex: string

Value

Represents a collection of tokens.

Constructor

Constructs a Value instance using a quantity of lovelace and an Assets instance.

new helios.Value(
    lovelace: bigint = 0n,
    assets: helios.Assets = new helios.Assets()
)

Static methods

fromCbor

Deserialize a CBOR encoded Value instance.

Shifts bytes to the next CBOR element.

helios.Value.fromCbor(bytes: number[]): helios.Value

fromData

Turns a UplcData instance into a Value. Throws an error if it isn't in the right format.

helios.Value.fromData(data: helios.UplcData): helios.Value

Getters

assets

Gets the Assets contained in the Value.

value.assets: helios.Assets

lovelace

Gets the lovelace quantity contained in the Value.

value.lovelace: bigint

Methods

add

Adds two Value instances together. Returns a new Value instance.

value1.add(value2: helios.Value): helios.Value

assertAllPositive

Throws an error if any of the Value entries is negative.

value.assertAllPositive(): void

eq

Checks if two Value instances are equal (Assets need to be in the same order).

value1.eq(value2: helios.Value): boolean

ge

Checks if a Value instance is strictly greater or equal to another Value instance.

value1.ge(value2: helios.Value): boolean

gt

Checks if a Value instance is strictly greater than another Value instance.

value1.gt(value2: helios.Value): boolean

sub

Substracts one Value instance from another. Returns a new Value instance.

value1.sub(value2: helios.Value): helios.Value

setLovelace

Mutates the quantity of lovelace in a Value.

value.setLovelace(lovelace: bigint): void

toCbor

Serialize a Value instance using CBOR.

value.toCbor(): number[]

toData

Returns a MapData representation of a Value.

value.toData(): helios.MapData

Wallet

An interface type for a wallet that manages a user's UTxOs and addresses. Notably implemented by Cip30Wallet and WalletEmulator.

Getters

usedAddresses

Gets a list of addresses which already contain UTxOs.

wallet.usedAddresses: Promise<helios.Address[]>

unusedAddresses

Gets a list of unique unused addresses which can be used to send UTxOs to.

wallet.unusedAddresses: Promise<helios.Address[]>

utxos

Gets the complete list of UTxOs sitting at the addresses owned by the wallet.

wallet.utxos: Promise<helios.UTxO[]>

Methods

isMainnet

Returns true if the wallet is connected to the mainnet.

wallet.isMainnet(): Promise<boolean>

signTx

Signs a transaction, returning a list of signatures needed for submitting a valid transaction.

wallet.signTx(tx: helios.Tx): Promise<helios.Signature[]>

submitTx

Submits a transaction to the blockchain. Returns the TxId.

wallet.submitTx(tx: helios.Tx): Promise<helios.TxId>

WalletEmulator

An emulated Wallet, created by calling emulator.createWallet().

This wallet only has a single private/public key, which isn't rotated. Staking is not yet supported.

Getters

address

Gets the Address of the wallet.

wallet.address: helios.Address

pubKeyHash

Gets the PubKeyHash of the wallet.

wallet.pubKeyHash: helios.PubKeyHash

usedAddresses

A list containing the emulated wallet's single address.

wallet.usedAddresses: Promise<helios.Address[]>

unusedAddresses

Returns an empty list in this case.

wallet.unusedAddresses: Promise<helios.Address[]>

utxos

Gets all the UTxOs controlled by the emulated wallet.

wallet.utxos: Promise<helios.UTxO[]>

Methods

isMainnet

Returns false in this case.

wallet.isMainnet(): Promise<boolean>

signTx

Signs a transaction, returning a list containing the single signature needed for submitting it.

wallet.signTx(tx: helios.Tx): Promise<helios.Signature[]>

submitTx

Submits a transaction to the emulated blockchain. Returns the TxId.

wallet.submitTx(tx: helios.Tx): Promise<helios.TxId>

WalletHelper

High-level helper class for instances that implement the Wallet interface.

Constructor

const helper = new helios.WalletHelper(wallet: helios.Wallet)

Getters

allAddresses

Concatenation of usedAddresses and unusedAddresses.

helper.allAddresses: Promise<helios.Address[]>

baseAddress

First Address in allAddresses.

helper.baseAddress: Promise<helios.Address>

changeAddress

First Address in unusedAddresses.

helper.changeAddress: Promise<helios.Address>

refUtxo

First UTxO in utxos. Can be used to distinguish between preview and preprod networks.

helper.refUtxo: Promise<helios.UTxO>

Methods

isOwnAddress

Returns true if the PubKeyHash in the given Address is controlled by the wallet.

helper.isOwnAddress(address: helios.Address): boolean

isOwnPubKeyHash

Returns true if the givenPubKeyHash is controlled by the wallet.

helper.isOwnPubKeyHash(pkh: helios.PubKeyHash): boolean

pickUtxos

Pick a number of UTxOs needed to cover a given Value. The coin selection strategy is to pick the smallest first (WiP).

Returns two lists. The first list contains the selected UTxOs, the second list contains the remaining UTxOs.

helper.pickUtxos(amount: helios.Value): Promise<[helios.UTxO[], helios.UTxO[]]>

pickCollateral

Picks a single UTxO intended for collateral.

helper.pickCollateral(amount: bigint = 2000000n): Promise<helios.UTxO>

Helios CLI

This chapter explains how to use the helios-cli, and how to build and submit transactions using cardano-cli.

Note: helios-cli is work-in-progress and can only be used for simple operations (compiling, evaluating parameters, calculating script addresses).

Setup

This section explains how to:

These steps require the following dependencies:

  • node
  • npm
  • docker

Install helios-cli

Dependencies:

  • node
  • npm

Install using npm:

$ sudo npm install -g @hyperionbt/helios-cli

Verify the installation using the following command:

$ helios version

Install cardano-node

You will need a Linux environment with docker for this.

We have provided convenient docker containers for running cardano-node.

$ git clone https://github.com/Hyperion-BT/cardano-node-wrappers
$ cd cardano-node-wrappers

Build and start a cardano-node docker container (non-persistent):

$ make build-preprod

$ make run-preprod # non-persistent, just to check if it works

Or persistent:

$ make run-preprod-persistent # runs in background with a persistent data volume

Alternative you can choose preview.

These commands will automatically download IOG's latest cardano-node image, and then create a named docker volume for storing the blockchain state.

Check that the cardano-node container is running using the following command:

$ docker ps

Take note of the container id.

You can stop the container any time:

$ docker stop <container-id>

We recommend using docker stop and not docker rm -f as it allows cardano-node processes to receive the more graceful SIGTERM signal (instead of just SIGKILL).

You can clean up stopped containers if you are running low on system resources:

$ docker system prune

About 30 seconds after starting the cardano-node container, /ipc/node.socket should've been created and you can start using cardano-cli to query the blockchain. If you are restarting the cardano-node after a major upgrade (eg. an HFC) it could take much longer though (an hour or more). If you are impatient you should launch the cardano-node container using the docker run command without the -d flag. This way you can follow the (re)sync progress in your terminal.

Poll for the blockchain sync status using the following command:

$ docker exec <container-id> cardano-cli query tip --testnet-magic 1097911063

The first time it can take up to 10 hours for your cardano-node to fully synchronize.

Wallet setup

Start an interactive shell in your cardano-node docker container:

$ docker exec -it <container-id> bash

Create the directory where we will store the wallet keys:

> mkdir -p /data/wallets

Create three wallets, each with an associated payment address:

> cd /data/wallets

> cardano-cli address key-gen \
  --verification-key-file wallet1.vkey \
  --signing-key-file wallet1.skey
> cardano-cli address build \
  --payment-verification-key-file wallet1.vkey \
  --out-file wallet1.addr \
  --testnet-magic $TESTNET_MAGIC_NUM
> cat wallet1.addr

addr_test1vqwj9w0...

> cardano-cli address key-gen \
  --verification-key-file wallet2.vkey \
  --signing-key-file wallet2.skey
> cardano-cli address build \
  --payment-verification-key-file wallet2.vkey \
  --out-file wallet2.addr \
  --testnet-magic $TESTNET_MAGIC_NUM

> cardano-cli address key-gen \
  --verification-key-file wallet3.vkey \
  --signing-key-file wallet3.skey
> cardano-cli address build \
  --payment-verification-key-file wallet3.vkey \
  --out-file wallet3.addr \
  --testnet-magic $TESTNET_MAGIC_NUM

Take note of the payment address of wallet 1.

Funding

Go to testnets.cardano.org/en/testnets/cardano/tools/faucet/ to add some funds.

After adding some funds, check the balance of the wallet 1's payment address:

> cardano-cli query utxo \
  --address $(cat /data/wallets/wallet1.addr) \
  --testnet-magic $TESTNET_MAGIC_NUM

...

The funding faucet is limited to one usage per day per user. So try to fund wallets 2 and 3 on other days.

Using helios-cli

Compiling

$ helios compile my_script.hl -o my_script.json

Optimization can be switched on using the --optimize (or -O) flag:

$ helios compile my_script.hl --optimize -o my_script.json

helios-cli automatically searches for modules in the current directory. Other directories can be included using the -I option:

$ helios compile my_script.hl -I ./my_modules/ -o my_script.json

Parameters can be set using the -D<param-name> <param-value> option:

$ helios compile my_script.hl -DMY_PARAM 100 -o my_script.json

Evaluating a parameter

$ helios eval my_script.hl MY_PARAM

Similar to the compile command, additional module directories can be included using -I.

Calculating a script address

helios-cli can calculate the address of a compiled script:

$ helios address my_script.json

For mainnet address the --mainnet (or -m) flag must be used:

$ helios address my_script --mainnet

Example: Always succeeds

Create a always_succeeds.hl script with the following code:

spending always_succeeds

func main(_, _, _) -> Bool {
  true
}

Compile the Always Succeeds script into its JSON representation:

$ helios compile always_succeeds.hl

{"type": "PlutusScriptV2", "description": "", "cborHex" :"52510100003222253335734a0082930b0a5101"}

Start an interactive shell in the cardano-node container and copy the content of the JSON representing the script:

$ docker exec -it <container-id> bash

> mkdir -p /data/scripts
> cd /data/scripts

> echo '{
  "type": "PlutusScriptV2", 
  "description": "", 
  "cborHex": "52510100003222253335734a0082930b0a5101"
}' > always-succeeds.json

Generate the script address:

> cardano-cli address build \
  --payment-script-file /data/scripts/always-succeeds.json \
  --out-file /data/scripts/always-succeeds.addr \
  --testnet-magic $TESTNET_MAGIC_NUM

> cat /data/scripts/always-succeeds.addr

addr_test1wpfvdtcvnd6yknhve6pc2w999n4325pck00x3c4m9750cdch6csfq

We need a datum, which can be chosen arbitrarily in this case:

> DATUM_HASH=$(cardano-cli transaction hash-script-data --script-data-value "42")
> echo $DATUM_HASH

9e1199a988ba72ffd6e9c269cadb3b53b5f360ff99f112d9b2ee30c4d74ad88b

We also need to select some UTxOs as inputs to the transaction. At this point we should have one UTxO sitting in wallet 1. We can query this using the following command:

> cardano-cli query utxo \
  --address $(cat /data/wallets/wallet1.addr) \
  --testnet-magic $TESTNET_MAGIC_NUM

TxHash             TxIx  Amount
-------------------------------------------------------------
4f3d0716b07d75...  0     1000000000 lovelace + TxOutDatumNone

4f3d... is the transaction id. The UTxO id in this case is 4f3d...#0.

We now have everything we need to build a transaction and submit it.

Let's send 2 tAda (2 million lovelace) to the script address:

> TX_BODY=$(mktemp)
> cardano-cli transaction build \
  --tx-in 4f3d...#0 \
  --tx-out $(cat /data/scripts/always-succeeds.addr)+2000000 \
  --tx-out-datum-hash $DATUM_HASH \
  --change-address $(cat /data/wallets/wallet1.addr) \
  --testnet-magic $TESTNET_MAGIC_NUM \
  --out-file $TX_BODY \
  --babbage-era

Estimated transaction fee: Lovelace 167217

> TX_SIGNED=$(mktemp)
> cardano-cli transaction sign \
  --tx-body-file $TX_BODY \
  --signing-key-file /data/wallets/wallet1.skey \
  --testnet-magic $TESTNET_MAGIC_NUM \
  --out-file $TX_SIGNED

> cardano-cli transaction submit \
  --tx-file $TX_SIGNED \
  --testnet-magic $TESTNET_MAGIC_NUM

Transaction successfully submitted

If you check the wallet 1 payment address balance after a few minutes you will noticed that it has decreased by 2 tAda + fee. Note the left-over UTxO id, we will need it to pay fees when retrieving funds.

You can also try to check the balance of the script address:

> cardano-cli query utxo \
  --address $(cat /data/scripts/always-succeeds.addr) \
  --testnet-magic $TESTNET_MAGIC_NUM

...

The table should list at least one UTxO with your specific datum hash.

We can now try and get our funds back from the script by building, signing and submitting another transaction:

> PARAMS=$(mktemp) # most recent protocol parameters
> cardano-cli query protocol-parameters --testnet-magic $TESTNET_MAGIC_NUM > $PARAMS

> TX_BODY=$(mktemp)
> cardano-cli transaction build \
  --tx-in <fee-utxo> \ # used for tx fee
  --tx-in <script-utxo> \
  --tx-in-datum-value "42" \
  --tx-in-redeemer-value <arbitrary-redeemer-data> \
  --tx-in-script-file /data/scripts/always-succeeds.json \
  --tx-in-collateral <fee-utxo> \ # used for script collateral
  --change-address $(cat /data/wallets/wallet1.addr) \
  --tx-out $(cat /data/wallets/wallet1.addr)+2000000 \
  --out-file $TX_BODY \
  --testnet-magic $TESTNET_MAGIC_NUM \
  --protocol-params-file $PARAMS \
  --babbage-era

Estimated transaction fee: Lovelace 178405

> TX_SIGNED=$(mktemp)
> cardano-cli transaction sign \
  --tx-body-file $TX_BODY \
  --signing-key-file /data/wallets/wallet1.skey \
  --testnet-magic $TESTNET_MAGIC_NUM \
  --out-file $TX_SIGNED

> cardano-cli transaction submit \
  --tx-file $TX_SIGNED \
  --testnet-magic $TESTNET_MAGIC_NUM

Transaction successfully submitted

If you now check the balance of wallet 1 you should see two UTxOs, and the total value should be your starting value minus the two fees you paid.

Note that collateral is only paid if you submit a bad script. Cardano-cli does extensive checking of your script though, and should prevent you from submitting anything faulty. So collateral is only really paid by malicious users.

Example: Time lock

The always-succeeds contract in the previous section isn't very useful. Something that is still simple, but has real-world applications, is a time-lock contract. Actors send UTxOs to the time-lock address with a datum that contains a lock-until time. An optional nonce can be included in the datum to allow only the actors who know the nonce value to retrieve the UTxOs. The wallet from which the original UTxOs were sent is also able to retrieve the UTxOs at any time.

The Helios script:

spending time_lock

struct Datum {
    lockUntil: Time
    owner:     PubKeyHash // can't get this info from the ScriptContext
    nonce:     Int
}

func main(datum: Datum, _, ctx: ScriptContext) -> Bool {
    tx: Tx = ctx.tx;
    now: Time = tx.time_range.start;
    returnToOwner: Bool = tx.is_signed_by(datum.owner);

    (now > datum.lockUntil) || returnToOwner
}
// end-of-main, anything that comes after isn't part of the on-chain script

// MY_DATUM parameters
const LOCK_UNTIL = 0 // seconds since 1970, set by cli
const OWNER = PubKeyHash::new(#) // set by cli
const NONCE = 42 // can be set by cli

// Helios can evaluate MY_DATUM into a data-structure that can be used to build a transaction
const MY_DATUM = Datum{
  lockUntil: Time::new(LOCK_UNTIL*1000),  // needs to be in milliseconds
  owner:     OWNER, 
  nonce:     NONCE
}

UTxOs can be sent into the time-lock script arbitrarily as long as the datum has the correct format. UTxOs can be retrieved any time by the wallet that initiated the time-lock. UTxOs can be retrieved after the time-lock by anyone who knows the datum.

Once we have written the script, we generate its JSON representation using helios-cli, and then calculate the script address using cardano-cli:

$ helios compile time_lock.hl

{"type": "PlutusScriptV2", "description": "", "cborHex": "5..."}
$ docker exec -it <container-id> bash

> echo '{
  "type": "PlutusScriptV1",
  "description": "",
  "cborHex": "5...",
}' > /data/scripts/time-lock.json

> cardano-cli address build \
  --payment-script-file /data/scripts/time-lock.json \
  --out-file /data/scripts/time-lock.addr \
  --testnet-magic $TESTNET_MAGIC_NUM

> cat time-lock.addr

addr_test1...

For the datum we need the PubKeyHash of the initiating wallet (i.e. the owner):

$ docker exec -it <container-id> bash

> cardano-cli address key-hash --payment-verification-key-file /data/wallets/wallet1.vkey

1d22b9ff5fc...

We also need a lockUntil time, for example 5 minutes from now. Now we can build the datum:

$ helios eval time_lock.hl MY_DATUM \
  -DOWNER "000102030405060708090a0b0c0d0e0f101112131415161718191a1b" \
  -DLOCK_UNTIL $(($(date +%s) + 300)) \
  -DNONCE 12345

{"constructor": 0, "fields": [{"int": 16....}, {"bytes": "0001020304..."}, {"int": 12345}]}

Now let's send 2 tAda to the script address using the datum we just generated:

$ docker exec -it <container-id> bash

> cardano-cli query utxo \
  --address $(cat /data/wallets/wallet1.addr) \
  --testnet-magic $TESTNET_MAGIC_NUM

...
# take note of a UTxO big enough to cover 2 tAda + fees

> DATUM=$(mktemp)
> echo '{"constructor": 0, "fields": [{"int": 16....}, {"int": 42}]}' > $DATUM

> DATUM_HASH=$(cardano-cli transaction hash-script-data --script-data-file $DATUM)

> TX_BODY=$(mktemp)
> cardano-cli transaction build \
  --tx-in <funding-utxo> \
  --tx-out $(cat /data/scripts/time-lock.addr)+2000000 \
  --tx-out-datum-hash $DATUM_HASH \
  --change-address $(cat /data/wallets/wallet1.addr) \
  --testnet-magic $TESTNET_MAGIC_NUM \
  --out-file $TX_BODY \
  --babbage-era

Estimated transaction fee: Lovelace 167217

> TX_SIGNED=$(mktemp)
> cardano-cli transaction sign \
  --tx-body-file $TX_BODY \
  --signing-key-file /data/wallets/wallet1.skey \
  --testnet-magic $TESTNET_MAGIC_NUM \
  --out-file $TX_SIGNED

> cardano-cli transaction submit \
  --tx-file $TX_SIGNED \
  --testnet-magic $TESTNET_MAGIC_NUM

Transaction successfully submitted

Wait for the transaction to propagate through the network, and query the script address to see the locked UTxO(s).

First thing we should test is returning the UTxO(s) back to wallet 1. For that we use the following transaction:

> PARAMS=$(mktemp) # most recent protocol params
> cardano-cli query protocol-parameters --testnet-magic $TESTNET_MAGIC_NUM > $PARAMS

> TX_BODY=$(mktemp)
> cardano-cli transaction build \
  --tx-in <fee-utxo> \ # used for tx fee
  --tx-in <script-utxo \
  --tx-in-datum-file $DATUM \
  --tx-in-redeemer-value <arbitrary-redeemer-data> \
  --tx-in-script-file /data/scripts/time-lock.json \
  --tx-in-collateral <fee-utxo> \ # used for script collateral
  --invalid-before <current-slot-no> \
  --required-signer /data/wallets/wallet1.skey \
  --change-address $(cat /data/wallets/wallet1.addr) \
  --tx-out $(cat /data/wallets/wallet1.addr)+2000000 \
  --out-file $TX_BODY \
  --testnet-magic $TESTNET_MAGIC_NUM \
  --protocol-params-file $PARAMS \
  --babbage-era

Estimated transaction fee: Lovelace ...

> TX_SIGNED=$(mktemp)
> cardano-cli transaction sign \
  --tx-body-file $TX_BODY \
  --signing-key-file /data/wallets/wallet1.skey \
  --testnet-magic $TESTNET_MAGIC_NUM \
  --out-file $TX_SIGNED

> cardano-cli transaction submit \
  --tx-file $TX_SIGNED \
  --testnet-magic $TESTNET_MAGIC_NUM

Transaction successfully submitted

Note that this transaction build command differs slightly from the Always succeeds script:

  • --invalid-before <current-slot-no> is needed so the transaction is aware of the current time (via the start of the valid time-range). It might seem weird to specify (an approximation of) the current time at this point, as someone might try to cheat the time-lock by specifying a time far into the future. But the slot-leader checks the time-range as well, and rejects any transaction whose time-range doesn't contain the current slot.
  • --required-signer <wallet-private-key-file> is needed so that getTxSignatories(tx) doesn't return an empty list.

The second thing we must test is claiming the time-locked funds from another wallet (eg. wallet 2). Let's assume that the time-lock script still contains the 2 tAda sent by wallet 1, and that sufficient time has passed. Wallet 2 can claim the UTxO(s) using the following commands:

> PARAMS=$(mktemp) # most recent protocol params
> cardano-cli query protocol-parameters --testnet-magic $TESTNET_MAGIC_NUM > $PARAMS

> TX_BODY=$(mktemp)
> cardano-cli transaction build \
  --tx-in <fee-utxo> \ # used for tx fee
  --tx-in <script-utxo> \
  --tx-in-datum-file $DATUM \
  --tx-in-redeemer-value <arbitrary-redeemer-data> \
  --tx-in-script-file /data/scripts/time-lock.json \
  --tx-in-collateral <fee-utxo> \ # used for script collateral
  --invalid-before <current-slot-no> \
  --change-address $(cat /data/wallets/wallet2.addr) \
  --tx-out $(cat /data/wallets/wallet2.addr)+2000000 \
  --out-file $TX_BODY \
  --testnet-magic $TESTNET_MAGIC_NUM \
  --protocol-params-file $PARAMS \
  --babbage-era

Estimated transaction fee: Lovelace ...

> TX_SIGNED=$(mktemp)
> cardano-cli transaction sign \
  --tx-body-file $TX_BODY \
  --signing-key-file /data/wallets/wallet2.skey \
  --testnet-magic $TESTNET_MAGIC_NUM \
  --out-file $TX_SIGNED

> cardano-cli transaction submit \
  --tx-file $TX_SIGNED \
  --testnet-magic $TESTNET_MAGIC_NUM

Transaction successfully submitted

cardano-cli should give an error if you try to submit this transaction before the lockUntil time. After that time it should succeed, and wallet 2 will receive the time-locked UTxO(s).

Advanced concepts

This chapter covers minting policies, exploits, some more complex scripts, and some recommendations for building DApps.

Still a work in progress...

Exploits

Double satisfaction

If a smart contract simply checks that a given value is sent to an address, then that condition could be satisfied in another smart contract at the same time. The output is only sent once, but the condition is thus satisfied multiple times in the same transaction.

This can be avoided by enforcing that the output value is sent to the given address with a given datum tag.

Intermediate representation

Helios scripts aren't compiled directly to UPLC. Rather they are compiled into an Intermediate Representation (IR). This section describes the components of the IR and how the simplification process works. This can be useful information for auditors of the Helios code-base.

The Helios IR is a typeless variant of Helios, where high-level syntactic constructs have been replaced by low-level equivalents (represented a class hierarchy with IRExpr as a base class):

  • IRNameExpr
  • IRLiteralExpr
  • IRConstExpr
  • IRFuncExpr
  • IRCallExpr
    • IRCoreCallExpr
    • IRUserCallExpr
      • IRAnonCallExpr
        • IRFuncDefExpr
      • IRNestedAnonCallExpr
  • IRErrorCallExpr

The remaining part of this page describes the IR syntax.

IRNameExpr

Any word matching of the regular expression [_a-zA-Z][_a-zA-Z0-9]*, except the following keywords: const, error, true, false.

IRLiteralExpr

  • true or false for a literal Bool
  • ##[0-9a-f]* for literal Data
  • #[0-9a-f]* for a literal ByteArray
  • ".*" for a literal String
  • [0-9]+ for a literal Int

IRConstExpr

Emitted by Helios const statements.

const(<expr>)

IRFuncExpr

(<arg-name>, <arg-name>, ...) -> {
    <body-expr>
}

IRCallExpr

IRCoreCallExpr

__core__<builtin-name>(<arg-expr>, <arg-expr>, ...)

IRUserCallExpr

<expr>(<arg-expr>, <arg-expr>, ...)

IRAnonCallExpr

(<arg-name>, <arg-name>, ...) -> {
    <body-expr>
}(<arg-expr>, <arg-expr>, ...)

IRNestedAnonCallExpr

(<arg-name>, <arg-name>, ...) -> {
    <body-expr>
}(<arg-expr>, <arg-expr>, ...)(<call-arg-expr>, ...)

IRFuncDefExpr

(<fn-name>) -> {
    <rest-expr>
}(
    (<arg-name>, <arg-name>, ...) -> {
        <body-expr>
    }
)

IRErrorCallExpr

error(".*")

IR simplication

Simplification of a compiled program is done at the IR level because it provides more context.

Simplification consists of the following steps:

  1. Evaluation of constants
  2. Simplify literals
  • Inline literals
  • Evaluate core calls with only literal args
  1. Simplify topology
  • Count all references
  • Inline definitions
  • Remove unused definitions
  • Combine nested functions
  • Eliminate cast/uncast function call combinations

Evaluation of constants

This is step is always performed (i.e. regardless of the value of simplify when calling compile()).

This step starts at the root of the syntax tree by calling evalConstants(), which is called recursively until IRConstExpr instances are found. A IRCallStack is filled with definitions in the process. IRConstExpr calls eval() recursively instead.

Note that all definitions must be added as deferred IRValue instances to the stack by IRAnonCallExpr, as they might be needed by inner IRConstExpr instances.

Simplify literals

Next we inline all literal arguments defined in higher scopes. Any IRCoreCallExpr instances with only literal arguments are also evaluated.

A single method is defined on IRExpr for this step: simplifyLiterals(map: Map<IRVariable, IRLiteralExpr>). The literals arguments of IRCallExpr must be simplified before the algorithm goes deeper in the AST. The literal arguments of IRAnonCallExpr are the ones that are inserted in the map passed to simplifyLiterals.

Many builtin functions (e.g. ifThenElse) can already be simplified if only some of the args are literals.

Simplify topology

Before starting this step, all references of each IRVariable must be registered.

Inline definitions

Care needs to be taken not to inline wherever there is recursion (i.e. loops).

Note: IRNameExpr instances can always be inlined.

Remove unused definitions

This is done inside IRAnonCallExpr instances.

Combine nested functions functions

(outer) -> {
  (inner) -> {
    ...
  }
}(a)(b)

Should be simplified to:

(outer, inner) -> {
  ...
}(a, b)

This is done inside IRNestedAnonCallExpr instances.

Minting native assets

One of Cardano's best features is supporting native multi-assets. These are user-created tokens on Cardano that have the same treatment as the native coin (Ada).

This might not seem like a big deal but this offers serious advantages over Ethereum tokens (ERC-20 and ERC-721). But to understand the benefits you have to understand how user-defined tokens work on non-UTxO blockchains.

ERC-20 Standard

On Ethereum, tokens are defined using the ERC-20 standard. In this standard tokens are managed by a contract that stores all of the token's metadata and all user balances in a mapping (hashmap) called _balances. All transfers of an ERC-20 token are function calls to the contract to modify the _balances.

Any error in the implementation of the ERC-20 standard can lead to the loss of user funds.

UTxO Native Assets

On Cardano and other eUTxO blockchains user-defined tokens are first-class. Tokens on Cardano are stored in token bundles which can contain Ada and any native asset. This allows Cardano to do in one transaction what would normally take multiple contract calls on Ethereum.

Note: A token bundle must always contain a minimum amount of Ada.

Minting Policies

Minting policies are a lot like spending scripts. These policies validate attempts to mint or burn a tokens of that policy.

There are a few key differences wrt. spending scripts:

  • Minting policies are not directly linked to any UTxO they are included in the minting transaction directly.
  • Minting policies take two arguments (the ScriptContext and the Redeemer), they have no input UTxO and therefore no Datum.

AssetClass

Native assets are identified by their AssetClass this is a combination of:

  • a MintingPolicyHash: the hash of the minting policy of the token. Sometimes referred to as the CurrencySymbol or the PolicyID.

  • a token name: this is used to distinguish different assets within the same policy (e.g. multiple NFTs using the same minting policy)

Note: The MintingPolicyHash of ADA is an empty ByteArray (#). Since nothing can hash to an empty string Ada is the only token that can't be minted/burned using a minting policy.

Signature based minting

This example shows a simple minting policy that allows minting tokens as long as the transaction is signed by an owner. The owner has a given PubKeyHash.

minting signed

const OWNER: PubKeyHash = PubKeyHash::new(#26372...)

func main(_, ctx: ScriptContext) -> Bool {
    ctx.tx.is_signed_by(OWNER)
}

Unique minting

NFTs (Non-Fungible tokens) are tokens resulting from a unique minting event. To make an NFT the minting policy must make sure that that policy can only be used once and that only one token can ever be minted. The token name for the NFT in this example will be called example_nft. Usually the amount of each token is restricted to 1.

There are two approaches for these kind of minting policies:

  • Deadline-based Approach
  • UTxO-based Approach

Deadline-based Approach

NFTs were available on Cardano before smart contracts (Mary Hardfork) and they were implemented using deadlines. The main idea is that the token can only be minted before a deadline which already passed. This ensures no new tokens will ever be minted. This is very easy to implement:

minting deadline_nft

const DEADLINE: Time = Time::new(1661665196132) // milliseconds since 1970


func main(_, ctx: ScriptContext) -> Bool {
	tx: Tx = ctx.tx;

    nft_assetclass: AssetClass = AssetClass::new(
		ctx.get_current_minting_policy_hash(), 
		"example-nft".encode_utf8()
	);

    value_minted: Value = tx.minted;

    value_minted == Value::new(nft_assetclass, 1) && tx.time_range.start < DEADLINE
}

UTxO-Based Approach

This method is based on an example in the Plutus Pioneer Program. This approach takes advantage of the fact that all UTxOs have a unique TxOutputId. A UTxO's TxOutputId is made up of the transaction hash of the transaction that made the UTxO and the index of the UTxO in the outputs of that transaction. It's a builtin type that is defined as:

struct TxOutputId {
    tx_id: TxId
    index: Int
}

So with this approach, we specify in the minting policy that the transaction minting the NFT must spend a UTxO with a specific output ID. Since a UTxO can only be spent once this means the token can only be minted once.

minting utxo_nft

const OUTPUT_ID: TxOutputId = TxOutputId::new(TxId::new(#1213), 1)

func main(_, ctx: ScriptContext) -> Bool {
	tx: Tx = ctx.tx;

    nft_assetclass: AssetClass = AssetClass::new(
		ctx.get_current_minting_policy(), 
		"example-nft".encode_utf8()
	);

    value_minted: Value = tx.minted;

    value_minted == Value::new(nft_assetclass, 1) && 
    tx.inputs
        .any((input: TxInput) -> Bool {input.output_id == OUTPUT_ID})

}

Vesting contract

To put what we've done so far to use we're going to build a simple 'vesting' contract. This contract will lock up some tokens owned by an owner for a beneficiary that can't be claimed until after a deadline. The owner can get their funds back if the deadline has not passed yet

Datum

The datum stores the PubKeyHash of the beneficiary and creator's wallets and the vesting deadline is represented as a Time.

struct Datum {
    creator: PubKeyHash
    beneficiary: PubKeyHash
    deadline: Time
}

Note: The Time type represents a POSIX time and for more info Helios Builtins.

Redeemer

enum Redeemer {
    Cancel
    Claim
}

There are two cases when the validator should return true:

  • Cancel

    In this case, the 'owner' wishes to cancel the contract and get back their funds. For a 'Cancel' to succeed the following have to be checked

    • The owner signed the transaction.
    • The deadline hasn't passed.
  • Vesting Claim

    A 'Claim' occurs when the 'beneficiary' wishes to claim the tokens vested for them. For it to be valid the following have to be checked:

    • The beneficiary signed the transaction.
    • The deadline has passed.

main

func main(datum: Datum, redeemer: Redeemer, context: ScriptContext) -> Bool {
    tx: Tx = context.tx;
    now: Time = tx.time_range.start;

    redeemer.switch {
        Cancel => {
            // Check that deadline hasn't passed
            now < datum.deadline && 

            // Check that the owner signed the transaction
            tx.is_signed_by(datum.creator)
        },
        Claim => {
           // Check that deadline has passed.
           now > datum.deadline &&

           // Check that the beneficiary signed the transaction.
           tx.is_signed_by(datum.beneficiary)
        }
    }
}

Complete code

spending vesting

struct Datum {
    creator: PubKeyHash
    beneficiary: PubKeyHash
    deadline: Time
}

enum Redeemer {
    Cancel
    Claim
}

func main(datum: Datum, redeemer: Redeemer, context: ScriptContext) -> Bool {
    tx: Tx = context.tx;
    now: Time = tx.time_range.start;

    redeemer.switch {
        Cancel => {
            now < datum.deadline &&
            tx.is_signed_by(datum.creator)
        },
        Claim => {
           now > datum.deadline &&
           tx.is_signed_by(datum.beneficiary)
        }
    }
}

English auction

In this example, we will rewrite the English auction contract from the Plutus Pioneer's program in Helios.

This validator can be used to lock assets that are to be auctioned in a UTxO.

main

spending english_auction

struct Datum {
    seller:         PubKeyHash
    bid_asset:      AssetClass     // allow alternative assets (not just lovelace)
    min_bid:        Int
    deadline:       Time
    for_sale:       Value          // the asset that is being auctioned
    highest_bid:    Int            // initialized at 0, which signifies the auction doesn't yet have valid bids
    highest_bidder: PubKeyHash

    func update(self, highest_bid: Int, highest_bidder: PubKeyHash) -> Datum {
        self.copy(highest_bid: highest_bid, highest_bigger: highest_bidder)
    }
}

enum Redeemer {
    Close 
    Bid {
        bidder: PubKeyHash
        bid: Int
    }
}

func main(datum: Datum, redeemer: Redeemer, ctx: ScriptContext) -> Bool {
    tx: Tx = ctx.tx;

    now: Time = tx.time_range.start;

    validator_hash: ValidatorHash = ctx.get_current_validator_hash();

    redeemer.switch {
        Close => {
            if (datum.highest_bid < datum.min_bid) {
                // the forSale asset must return to the seller, what happens to any erroneous bid value is irrelevant
                tx
                  .value_sent_to(datum.seller)
                  .contains(datum.for_sale + datum.highest_bid) &&
                // Check that the deadline has passed
                now > datum.deadline                                    
            } else {
                // Check that the seller receives the highest bid
                tx
                  .value_sent_to(datum.seller)
                  .contains(Value::new(datum.bid_asset, datum.highest_bid))    &&
                // Check that highest bidder is given the token being auctioned
                tx
                  .value_sent_to(datum.highest_bidder)
                  .contains(datum.for_sale)                                    &&
                // Check that the deadline has passed
                now > datum.deadline                                    
            }
        },
        b: Bid => {
            if (b.bid < datum.min_bid) {
                false
            } else if (b.bid <= datum.highest_bid) {
                false
            } else {
                // first bid is placed by the auction creator
                expected_datum: Datum = datum.update(b.bid, b.bidder);

                // Check that new Auction UTxO contains the token for sale and the new bid
                tx
                  .value_locked_by_datum(validator_hash, expected_datum)
                  .contains(datum.for_sale + Value::new(datum.bid_asset, b.bid)) &&
                // Check that the old highest bidder is repayed
                tx
                  .value_sent_to(datum.highest_bidder)
                  .contains(Value::new(datum.bid_asset, datum.highest_bid))      &&
                // Check that the deadline hasn't passed
                now < datum.deadline
            }
        }
    }
}

Oracle pools (WiP)

Oracles post information on the blockchain periodically (eg. the ADA/USD exchange rate), and thanks to the recent Vasil upgrade it has become easier to use that information in smart contracts (eg. stable-coin contracts).

Of course relying on a single centralized oracle goes against the spirit of cryptocurrencies and blockchain technology, so oracles should be grouped in pools. An oracle pool is essentially a smart contract which periodically groups inputs from all participating oracles into a single UTxO. The participating oracles are also rewarded from the oracle pool contract.

A more complete description of oracle pools can be found (here)[https://github.com/Emurgo/Emurgo-Research/blob/master/oracles/Oracle-Pools.md].

Oracle pool design decisions

Membership

The first question we need to answer is who can be a member of the pool:

  1. new members can be voted in by existed members, malicious members can be voted out
  2. member performance is measured on-chain, and only the best performing X oracles can claim rewards, in case of a draw seniority wins, X can be changed by voting
  3. token-based membership

Of course the concept of individual 'members' doesn't really apply to an anonymous blockchain, as a single physical actor can control multiple memberships.

Membership based purely on voting allows the founder(s) to keep exerting strong control over the oracle pool. Initial membership distribution is also entirely obscured from the public. So closed or limited membership is probably a bad idea from a decentralization perspective.

Entry into the pool based on performance is essentially a time and resource intensive method of acquiring membership. Existing members will vote to keep the max number of members low, in order maximize individual rewards. To avoid that the max number of members might need to be fixed via the contract, but that way the contract loses a lot of flexibility.

Token-based membership is vulnerable to a 51% attack. An attacker could quietly acquire the majority of tokens. Any subsequent attack would instantly destroy all smart contracts relying on the oracle. Because oracle pools are expected to be critical infrastructure for DeFi on Cardano, such an attack must of course be avoided at all costs. So that means initial token distribution must be spread very well, for which several rounds of ISPOs can be used. There also needs to be high enough oracle operation reward, so the tokens are effectively staked and aren't floating around on exchanges.

Note that oracle pools with open membership are also vulnerable to 51% attacks, but that such attacks are made more difficult by the time-delay of requiring the membership.

For this example we will choose a token-based membership. So the first task will be minting the tokens (see (how-to guide to mintin)[tutorial_06-minting_policy_scripts.md]).

Data-point collection

Data-point submission happens in three phases.

An active oracle must own some oracle tokens. Every posting period it calculates the data-point and sends a UTxO into the oracle pool contract. The data-point is described in a conventional hash, along with salt. The posting UTxO must also contain the oracle tokens and sufficient collateral.

In a second phase each participating oracle resends the UTxO into the oracle pool contract, while adding a provable time-stamp to the datum.

In the last phase the datum is 'unhidden' by resending the UTxO into the oracle pool contract with an inline-datum. At this point the script can check if sufficient time has passed since unhiding. The 'unhidden' UTxO must also be registered in a special registration UTxO (of which there can be multiple for parallel posting).

Note that data-point submission also contains governance parameters.

Data-point aggregation

This is the most complex transaction of the oracle pool contract.

This transaction can be submitted after a predefined period after the first entry in one of the registration UTxOs.

In this transaction all 'unhidden' UTxOs, with a time-stamp lying in the correct range, are used to resend the data carrying UTxO into the script contract with the new data-point (inline-datum of course). The submitting oracle must use all the 'unhidden' UTxOs that have been registered in the registration UTxOs. A token-weighted median of the data-point is calculated. Any oracles that lie within a predefined range of the median receive rewards according to how many oracle tokens they own (the submitting transaction gets double the rewards). Any oracles that lie outside the predefined range lose their collateral to the contract. The registration UTxOs are emptied, and the oracle tokens/left-over collateral is sent back to the owners.

The uniqueness of each input UTxO datum must also be checked. Two or more UTxOs with the same datum are obviously colluding (they would've had to have picked the same salt) and lose their collateral.

The final data-point UTxO can also contain governance parameters, which are updated if there is a sufficient majority. One of these parameters is the number of registration UTxOs, for which additional ones need to be minted if the number increases, and superfluous ones need to burned if the number decreases.

The final data-point UTxO must be marked by a unique data-NFT.

Minting

The oracle pool described above requires minting 3 different kinds of tokens:

  1. oracle pool membership tokens (unique minting)
  2. a single data NFT (unique minting)
  3. registration NFT (non-unique minting/burning)

Governance parameters

  • submissionPeriod Duration (period in which the first two phases of data submission must be completed, unhiding must happen after this period)
  • unhidingPeriod Duration (period in which data-point UTxOs are unhidden and registered in registration UTxOs)
  • nRegistrationQueues Integer (number of registration UTxOs)
  • collateralPerMembershipToken Integer (collateral asset will probably be ADA)
  • validDataRange Integer (+- around the median, could be in 'points', so '1' is 1%)
  • governanceQuorum Integer (could be '75' for 75%)
  • postRewardForWholePool Integer (postReward asset will probably be ADA)
  • extraRewardForPoster Integer (could be '100' for 100% extra i.e. x2)

The datum of the data-point UTxO will contain the governance parameters and the data-point itself:

  • dataPoint Integer

This could be extended to multiple data-points at some, although makes (dis)incentives more difficult to calulate.

Other datums

Registration UTxO datum:

  • inputs []TxOutputId

Submission UTxO datum:

  • owner PubKeyHash
  • salt Integer
  • dataPoint Integer
  • all the governance parameters

The sometimes vastly differing datum types probably made it worthwhile to introduce union types:

enum Datum {
    Post {
        dataPoint: Integer,
        govParams: GovernanceParams
    }, 
    Submit {
        owner:     PubKeyHash,
        salt:      Integer,
        dataPoint: Integer,
        time:      Time,
        govParams: GovernanceParams
    },
    Queue {
        inputs: []TxOutputId
    }
}

Data constructors must have a unique order, and can only be used in a single union. The data constructor type can be referenced using the :: symbol (eg. Datum::Post).

DApp recommendations

No build tools

A lot of care went into making the Helios library as auditable as possible. Therefore we recommend for your DApp to use the library in its unminified form so users can audit the compiler more easily.

We also recommened using an client-side UI framework (like Preact/Htm), so that your DApp can be served directly to the client without needing a build-step.

Show contract button

The smart contracts used in the DApp should be viewable by the user.

Tx finalization

The Helios API can be used to build transactions. Finalization consist of the following steps:

  1. balancing of non-ADA assets
  2. calculation of script execution units (using a dummy fee set to the maximum possible value)
  3. setting collateral inputs and collateral output (using total execution budget calculated in previous step)
  4. iteratively calculate the min fee for the transaction and balance the lovelace