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:
- Learning Ergo 101 : eUTXO explained for human beings, a great blog post by David Pryzbilla
Changelog
This page documents breaking changes and major features of major version releases
v0.13
Language
copy
automatically defined on user structs and enum-variants- variable arguments in
main
deprecated - function optional arguments with default values
- function calls with named arguments
API
helios.Int
renamed tohelios.HInt
helios.HeliosString
renamed tohelios.HString
helios.HeliosMap
renamed tohelios.HMap
helios.List
renamed tohelios.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
orfalse
)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 toMaybe
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 theget
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:
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
- Void functions
- Anonymous functions
- Unused arguments
- Optional arguments
- Named arguments
- Function values
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:
- if
n == 1
the sequence ends - if
n
is even the next number isn / 2
- 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:
Operator | Precedence |
---|---|
- (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:
Int
ByteArray
[]Data
Map[Data]Data
- any user-defined enum, or
(Int, []Data)
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. Theself
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 UTxOScriptContext
: 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
andRedeemer
.
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:
OutputDatum::new_hash
OutputDatum::new_inline
OutputDatum::new_none
ScriptContext::new_certifying
ScriptContext::new_minting
ScriptContext::new_rewarding
ScriptContext::new_spending
Tx::new
TxInput::new
TxOutput::new
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
- AssetClass (i.e. the kind of 'currency')
- Value
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
- DatumHash
- MintingPolicyHash
- PubKey
- PubKeyHash
- ScriptHash
- StakeKeyHash
- StakingValidatorHash
- ValidatorHash
Transaction types
- Address
- Credential
- DCert
- OutputDatum
- ScriptContext
- ScriptPurpose
- StakingCredential
- StakingHash
- StakingPurpose
- Tx
- TxId
- TxInput
- TxOutput
- TxOutputId
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 aByteString
, but that name was deemed too ambiguous for average programmers soByteArray
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 ByteArray
s.
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:
PubKey
(wrapsPubKeyHash
)Validator
(wrapsValidatorHash
)
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 aStakingCredential
Deregister
: deregister aStakingCredential
Delegate
: delegate aStakingCredential
to a poolRegisterPool
: register a poolRetirePool
: 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
head
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 afterhead
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 then
-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 returntrue
.
!=
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:
StakeKey
(wrapsStakeKeyHash
)Validator
(wrapsStakingValidatorHash
)
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 Time
s 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
anddatums
fields can be any type when instantiating a newTx
instance. But when getting theredeemers
and thedatums
the value type is actuallyData
(seeredeemers
anddatums
).
Getters
inputs
Returns the list of TxInput
s 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 TxOutput
s 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 DCert
s of the transaction (i.e. list of staking certifying actions).
tx.dcerts -> []DCert
withdrawals
Returns a map of staking reward withdrawals. The map value Int
s 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 DatumHash
es 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 TxOutput
s sent to a regular payment address.
tx.outputs_sent_to(pkh: PubKeyHash) -> []TxOutput
outputs_sent_to_datum
Returns the TxOutput
s 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 TxOutput
s being locked at the given script address.
tx.outputs_locked_by(script_hash: ValidatorHash) -> []TxOutput
outputs_locked_by_datum
Returns the TxOutput
s 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 ifvalue
contains something, but in that case it is usually better to use thevalue.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 Value
s are the same.
Value == Value -> Bool
Note: the assets and tokens must also be in the same order for
==
to returntrue
.
!=
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 Value
s. 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
istrue
the resulting program is optimized for production. Ifsimplify
isfalse
no optimizations are performed and
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 UTxO
s 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
andhexToBytes
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
- generating datums
Contract
helper class- creating a new sale
- canceling a sale
- buying for-sale assets
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 aUplcValue
(i.e. the result of aevalParam
call). It doesn't take an arbitrary object however, as that might get confused for the internals of aUplcValue
.
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 ofUplcData
. The otherUplcData
child-types are:IntData
,ByteArrayData
,ListData
andMapData
.
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
Address
Assets
BlockfrostV0
ByteArrayData
CborData
Cip30Wallet
CoinSelection
ConstrData
Datum
DatumHash
FuzzyTest
HeliosData
IntData
ListData
MapData
MintingPolicyHash
NetworkEmulator
NetworkParams
Program
PubKeyHash
Signature
Tx
TxId
TxOutput
TxRefInput
TxWitnesses
UplcBool
UplcByteArray
UplcData
UplcDataValue
UplcInt
UplcPair
UplcProgram
UplcString
UplcUnit
UplcValue
UserError
UTxO
ValidatorHash
Value
WalletEmulator
WalletHelper
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
, Address
es 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
orValidatorHash
) - 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 MintingPolicyHash
es).
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 UTxO
s 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:
Address
Bool
ByteArray
DatumHash
HMap
HString
HInt
HList
Option
PubKeyHash
StakeKeyHash
StakingValidatorHash
TxId
TxOutputId
ValidatorHash
Value
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:
- Preview: https://d1t0d7c2nekuk0.cloudfront.net/preview.json
- Preprod: https://d1t0d7c2nekuk0.cloudfront.net/preprod.json
- Mainnet: https://d1t0d7c2nekuk0.cloudfront.net/mainnet.json
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:
- Header (1 byte, see CIP 8)
- Staking witness hash (28 bytes that represent the
StakeKeyHash
orStakingValidatorHash
)
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 UTxO
s 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 UTxO
s 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 UTxO
s 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 thatgetTxSignatories(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
orfalse
for a literalBool
##[0-9a-f]*
for literalData
#[0-9a-f]*
for a literalByteArray
".*"
for a literalString
[0-9]+
for a literalInt
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:
- Evaluation of constants
- Simplify literals
- Inline literals
- Evaluate core calls with only literal args
- 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 theRedeemer
), 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 emptyByteArray
(#
). 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:
- new members can be voted in by existed members, malicious members can be voted out
- 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
- 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:
- oracle pool membership tokens (unique minting)
- a single data NFT (unique minting)
- 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:
- balancing of non-ADA assets
- calculation of script execution units (using a dummy fee set to the maximum possible value)
- setting collateral inputs and collateral output (using total execution budget calculated in previous step)
- iteratively calculate the min fee for the transaction and balance the lovelace