The OpShin Book

Introduction to the OpShin programming languages

Disclaimer: This book is still WIP, so expect incomplete or missing information.

Opshin is a pythonic language for writing smart contracts on the Cardano blockchain. The goal of Opshin is to reduce the barrier of entry in Smart Contract development on Cardano. Opshin is a strict subset of Python, this is means anyone who knows Python can get up to speed on Opshin pretty quickly.

What is a Smart Contract on Cardano?

On Cardano, funds are stored at addresses. An address can be controlled by a cryptographic key, the secret for unlocking it being only known to a specific user. The alternative is a Smart Contract - this is some encoded logic that controls unlocking funds from the address. In particular, this logic just validates that funds may be unlocked from the address based on how they are spent. For this reason, this kind of smart contract is often referred to as Spending Validator.

Other contracts also exist i.e. to validate minting of native tokens, withdrawal of stake rewards or certification of stake pools.

Sample Code

The following contract validates that the signature of a special user is present in a transaction. The signature is specified by a third party and attached to an UTxO sent to the contract. The receiver can then construct a transaction where they unlock the funds from the contract. The contract allows this when it is provided the signature of the receiver.

# gift.py
from opshin.prelude import *


@dataclass()
class WithdrawDatum(PlutusData):
    pubkeyhash: bytes


def validator(datum: WithdrawDatum, redeemer: None, context: ScriptContext) -> None:
    sig_present = False
    for s in context.tx_info.signatories:
        if datum.pubkeyhash == s:
            sig_present = True
    assert sig_present, "Required signature missing"

Why opshin?

  • 100% valid Python. Leverage the existing tool stack for Python, syntax highlighting, linting, debugging, unit-testing, property-based testing, verification
  • Intuitive. Just like Python.
  • Flexible. Imperative, functional, the way you want it.
  • Efficient & Secure. Static type inference ensures strict typing and optimized code.

Supporters

The main sponsor of this project is Inversion. Here is a word from them!

At Inversion, we pride ourselves on our passion for life and our ability to create exceptional software solutions for our clients. Our team of experts, with over a century of cumulative experience, is dedicated to harnessing the power of the Cardano blockchain to bring innovative and scalable decentralized applications to life. We've successfully built applications for NFT management, staking and delegation, chain data monitoring, analytics, and web3 integrations, as well as countless non-blockchain systems. With a focus on security, transparency, and sustainability, our team is excited to contribute to the Cardano ecosystem, pushing the boundaries of decentralized technologies to improve lives worldwide. Trust Inversion to be your go-to partner for robust, effective, and forward-thinking solutions, whether blockchain based, traditional systems, or a mix of the two.

They have recently started a podcast, called "Africa On Chain", which you can check out here: https://www.youtube.com/@africaonchain

Getting Started with Opshin

Prequisites

This guide tries to assume as little knowledge as possible but there are certain assumptions:

  • You should understand Python. Opshin is basically Python so we assume some basic knowledge of Python.

Installation

Install Python 3.8, 3.9, 3.10 or 3.11. Then run:

python3 -m pip install opshin

Compiling Opshin Code

  1. Make a file called hello_world.py and copy:

    # hello_world.py
    
    def validator(_: None) -> None:
        print("Hello world!")
    
  2. Run this command:

    $ opshin build hello_world.py
    

    This should create a build folder in the current directory. The build folder should look like this:

    build/
      └-hello_world/
        ├-mainnet.addr
        ├-script.cbor
        ├-script.plutus
        ├-script.policy_id
        └-testnet.addr
    

    We'll cover what all these files in the hello_world sub-folder mean later in the book.

Compatibility

All OpShin versions are tightly tied to specific versions of PyCardano. Due to a change of the default value of constructor ids, all OpShin versions < 0.20.0 are only compatible with PyCardano < 0.10.0. All versions >= 0.20.0 are only compatible with PyCardano >= 0.10.0.

eUTxO Crash Course

Note: This is based on an awesome guide by @Ktorz

If you have absolutely no idea what developing on Cardano looks like, worry not. You just found the right piece to get started. Opshin is a language that makes on-chain programming easy. But what is "on-chain programming" to begin with? While this succinct documentation piece has no ambition to be a complete course on blockchains, it should give you enough insights to build a basic understanding of the fundamentals.

Note: This course will reference cryptography concepts such as hash digests or digital signatures. We, therefore, expect readers to be either familiar with those concepts (at least a tiny bit) or to read up on them. There are plenty of resources available in the wild regarding cryptography and this crash course isn't one of them.

Blocks & transactions

Blockchains are made of blocks. And blocks are made of transactions. Without going into the details, you can think of blocks as being objects divided into two parts: a header and a body. The header contains information about the blocks, such as who produced them and when they were made. The body is nothing more than an ordered sequence of transactions.

Note that the "chain" of blockchain comes from how blocks reference one another. Indeed, each block header includes at least two things:

  • A hash digest of the block body
  • A hash digest of the previous block header
┏━ Header ━━━━━━━━━━━━━━┳━ Body ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃                       ┃                                           ┃
┃  Body hash            ┃ ┌────────────────┬────────────────┬─────┐ ┃
┃  Previous header hash ┃ │ Transaction #1 │ Transaction #2 │ ... │ ┃
┃  Timestamp            ┃ └────────────────┴────────────────┴─────┘ ┃
┃                       ┃                                           ┃
┗━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

A hash digest is a tamper-proof mechanism that maps an input value to a fixed-sized output. Think of it as a way to assign an identifier to a piece of content, such that the identifier depends on the content itself: change the content, change the identifier.

A chain is formed by including in every block header:

  • a hash of the block body; and
  • a header hash of the previous block.

Changing any transaction in a block will change the block body hash, thus changing the block header hash, the header hash of the next block, and so on, invalidating the entire chain that follows.

        ____          ____          ____          ____          ____
       /    /\       /    /\       /    /\       /    /\       /    /\
o ❮❮  /____/  \ ❮❮  /____/  \ ❮❮  /____/  \ ❮❮  /____/  \ ❮❮  /____/  \ ...
      \    \  /     \    \  /     \    \  /     \    \  /     \    \  /
╿      \____\/       \____\/       \____\/       \____\/       \____\/
│
│                                     ╿
│                                     │
└ Genesis configuration               │
                                      └ Block

A transaction is, therefore, the most fundamental primitive on blockchains. They are the mechanism whereby users (a.k.a you) can take actions to change the state of the blockchain. A chain starts from an initial state typically referred to as genesis configuration. And from there, transactions map a previous state into a new state. Finally, blocks are merely there to batch transactions together.

Unspent Transaction Outputs

In the traditional database world, a transaction is a means to bundle together a series of atomic operations so that all are successful or none happen. In the financial world, it is a way to transfer assets from one location to another.

In the blockchain world, it is a bit of both.

A transaction is, first and foremost, an object with an input from where it takes assets and an output to where it sends them. Often, as is the case in Cardano, transactions have many inputs and many outputs. And, in addition to inputs and outputs, blockchain protocols often include other elements that modify different parts of the blockchain state (e.g. delegation certificates, governance votes, user-defined assets definitions...)

Moreso, like in the database world, a transaction is an all-or-nothing atomic series of commands. Either it is valid, and all its changes are applied, or it isn't, and none are applied.

We'll talk more about other capabilities later. For now, let's focus on inputs and outputs, starting with the outputs.

Outputs

In Cardano, an output is an object that describes at least two things:

  • a quantity of assets -- also known as, a value;
  • a condition for spending (and/or delegating) those assets -- also known as an address.

In addition, a data payload can also be added to outputs but let's not bother with that just now. The role of the value is pretty transparent, and it indicates how many assets hold the output.

Incidentally, Cardano supports two kinds of assets: the main protocol currency (a.k.a. Ada); and user-defined currencies. Both live side-by-side in values though slightly different rules apply to each.

The address captures the logic that tells the protocol under what conditions one can utilize the assets at a particular output. It is what defines ownership of the assets. We'll explore this very soon. Bear with us a little more.

┏━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃         ┃           ┃                ┃
┃  Value  ┃  Address  ┃  Data payload  ┃
┃         ┃           ┃                ┃
┗━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛

Inputs

An input is a reference to a previous output. Think of outputs as post-it notes with a unique serial number and inputs as being this serial number.

A transaction is a document indicating which post-it notes should be destroyed and which new ones should be pinned to the wall. Note that there are rules regarding the construction of transactions. For example. there must as much value in as there's value out. Said differently, the total value should balance out but might be shuffled differently.

An output that hasn't been spent yet (i.e. is still on the wall) is called -- you guessed it -- an unspent transaction output, or UTxO in short. The blockchain state results from looking at the entire wall of post-it remaining notes. It includes not only the available UTxO, but also any additional data defined by the protocol.

Okay, back to inputs.

Technically speaking, an input's "serial number" is the hash digest of the transaction that emitted the output it refers to and the position of the output within that transaction. These two elements make each input unique. And because outputs are removed from the available set (post-it note is destroyed) when spent, they can only be spent once. At least, that's what the blockchain protocol makes sure of.

┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃                    ┃                ┃
┃  Transaction hash  ┃  Output index  ┃
┃                    ┃                ┃
┗━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛

Where do the first outputs come from?

If you've carefully followed the narrative we just went through, you might have realized that we have a chicken-and-egg situation. Inputs are references to outputs. And outputs are created by spending inputs.

This is what the genesis configuration is for. It defines the starting point of the blockchain in the form of an agreed-upon initial list of outputs. Those outputs can be referred to using some special identifiers. For example, the genesis configuration hash digest and the output's position in the configuration.

TL;DR

Let's quickly recap what we've seen so far:

  • A blockchain has an initial state called a genesis configuration;
  • A transaction captures instructions to modify that state (e.g. transfer of assets);
  • A block batches transactions together and has a reference to a parent block;
  • Assets movement are expressed using inputs and outputs in transactions;
  • An output is an object with at least an address and a value;
  • An address describes the conditions needed to use the value associated to it;
  • An input is a reference to a previous output.

Addresses

Overview

It is now time to delve more into Cardano addresses. A typical address is made of 2 or 3 parts:

┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃          ┃                       ┃                          ┃
┃  Header  ┃  Payment credentials  ┃  Delegation credentials  ┃
┃          ┃                       ┃                          ┃
┗━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━┛

We said 2 or 3 because the last part -- the delegation credentials -- is optional. The first part is called the Header, and it describes the type of address (i.e. what comes next) and the network within which this address can be used. We call that last bit a network discriminant and it prevents silly mistakes like sending real Mainnet funds to a test address. An address is represented as a sequence of bytes, usually encoded using bech32 or simply base16 text strings.

For example:

addr1x8phkx6acpnf78fuvxn0mkew3l0fd058hzquvz7w36x4gt7r0vd4msrxnuwnccdxlhdjar77j6lg0wypcc9uar5d2shskhj42g

or alternatively

31c37b1b5dc0669f1d3c61a6fddb2e8fde96be87b881c60bce8e8d542fc37b1b5dc0669f1d3c61a6fddb2e8fde96be87b881c60bce8e8d542f

We can dissect the latter text string to make the three parts mentionned above more apparent:

               Type = 3 ┐┌ Network = 1 (1 = mainnet, 0 = testnet)
                        ││
                        ╽╽
Header:                 31
Payment credentials:    c37b1b5dc0669f1d3c61a6fddb2e8fde96be87b881c60bce8e8d542f
Delegation credentials: c37b1b5dc0669f1d3c61a6fddb2e8fde96be87b881c60bce8e8d542f

As we can see, this address is a type 3, is for mainnet and uses the same credentials for both the payment and the delegation part.

You will often need to convert back-and-forth between bech32-encoded strings and hex-encoded strings. A great command-line tool for working with bech32 can be found at input-output-hk/bech32. Use it!

Payment credentials

The next part is the payment credentials, also called the payment part. This is what describes the spending conditions for the address. Remember how UTxOs are like post-it notes on a wall? Yet you don't get to hang them or pick them up directly yourself. You have to hand over a transaction to the network validators. Imagine an employee who's gatekeeping the wall of post-it notes and to whom you must give a form that describes what you want to do. Each post-it note has written on it the conditions one must meet to pick it up and destroy it. That's what the payment credentials are for in the address. They come in one of two forms:

  • a verification key hash digest; or
  • a script hash digest.

In the first form, the validator nodes -- or the employee -- will ask you to provide a digital signature from the signing key corresponding to the verification key. This approach relies on asymmetric cryptography, where one generates credentials as a public (verification) and private (signing) key pair. In the address, we store only a hash digest of the verification key for conciseness and to avoid revealing it too early (even though it is public material). When spending from such an address, one must reveal the public key and show a signature of the entire transaction as witnesses (a.k.a proofs). This way of specifying spending conditions is relatively straightforward but also constrained because it doesn't allow for expressing any elaborate logic.

This is where the second form gets more interesting. Cardano allows locking funds using a script representing the validation logic that must be satisfied to spend funds guarded by the address. We typically call such addresses: script addresses. Similarly to the first form, the entire script must be provided as a witness by any transaction spending from a script address, as well as any other elements required by the script. Scripts are like predicates. Said differently, they are functions that return a boolean value: True or False. To be considered valid, all scripts in a transaction must return True. We'll explore how this mechanism works in a short moment.

Delegation credentials

Addresses may also contain delegation credentials, also called a delegation part. We will only go a little into the details but think of the delegation credentials as a way to control what can be done with the stake associated with the address. The stake corresponds to the Ada quantity in the output's value that the consensus protocol counts to elect block producers. In Cardano, the stake can be delegated to registered entities called stake pools. By delegating, one indicates that the stake associated with an output should be counted as if it belonged to the delegatee, increasing their chance of producing a block. In return, the delegatee agrees to share a portion of their block-producing rewards with the delegator.

While the payment credentials control how to spend an output, delegation credentials control two separate operations:

  • how to publish a delegation certificate (e.g. to delegate stake to a stake pool);
  • how to withdraw rewards associated with the stake credentials.

Like payment credentials, delegation credentials comes in two forms: as verification key hash digest or as script hash digest.

More information about addresses and how they work can be found in CIP-0019

TL;DR

                        ┌ For spending
                        │
                        ╽
┏━ Header ━━━━━━━━━━━━━┳━ Payment credentials ━━━━━━━┳━ Delegation credentials ━━━━┓
┃                      ┃                             ┃                             ┃
┃                      ┃  ┌───────────────────────┐  ┃  ┌───────────────────────┐  ┃
┃  ┌──────┬─────────┐  ┃  │ Verification key hash │  ┃  │ Verification key hash │  ┃
┃  │ Type │ Network │  ┃  ├────────── OR ─────────┤  ┃  ├────────── OR ─────────┤  ┃
┃  └──────┴─────────┘  ┃  │      Script hash      │  ┃  │      Script hash      │  ┃
┃                      ┃  └───────────────────────┘  ┃  └───────────────────────┘  ┃
┃                      ┃                             ┃                             ┃
┗━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
                                                      ╿
                                                      │
                                                      └ For:
                                                          - publishing certificates
                                                          - withdrawing rewards

Before we move on, let's recap again:

  • An address is made of 2 or 3 parts: a header, payment credentials and delegation credentials;
  • The header describes the type of address and the network it is for;
  • The last part, the delegation credentials, is optional though highly recommended;
  • Credentials (payment or delegation) take one of two forms:
    • a verification key hash;
    • a script hash;
  • Payment credentials control how to spend from an address;
  • Delegation credentials control how to publish certificates and how to withdraw rewards;
  • A script allows the definition of arbitrary validation logic.

Scripts, Datums and Redeemers

Overview

Hang in there! We are almost at the end of this crash course. We've seen what a UTxO is what an address is made of. And we spoke a bit about scripts. In particular, we said that scripts are like predicates, that is, pure functions (in the Mathematical sense) that takes a transaction as an argument and return either True or False.

Well, not exactly. We lied to you (only a tiny bit).

If we only had that, it would be hard to express more elaborate logic. In particular, capturing a state, which programs often require, would be infeasible. A state and transitions from that state. This is where the extended UTxO model strikes in. It adds two new components to what we've already seen: datums and redeemers.

We mentioned the datum earlier without calling it a datum when we said that outputs contained a value, an address and a data payload. This is what the datum is, a free payload that developers can use to attach data to script execution. When a script is executed in a spending scenario, it receives not only the transaction as context but also the datum associated with the output being spent.

The redeemer, on the other hand, is another piece of data that is also provided with the transaction for any script execution. Notice that the datum and redeemer intervene at two distinct moments. A datum is set when the output is created (i.e. when the post-it note is hung on the wall, it is part of the note). Whereas the redeemer is provided only when spending the output (i.e. with the form handed over to the employee).

Analogy

Another way to look at scripts, datums and redeemers is to think of them as a parameterised mathematical function.

             Script
          ╭─────────╮
    f(x) = x * a + b  = true | false
           ╿   ╿   ╿
  Redeemer ┘   │   │
               └─┬─┘
               Datum

The script defines the function as a whole. It indicates how the parameters and arguments are combined to produce a boolean outcome. The datum corresponds to the parameters of the function. It allows configuring the function and re-using a similar validation logic with different settings. Both the function and the parameters are defined when assets are locked in an output. Which leaves only the function argument to be later provided. That argument is the redeemer (as well as the rest of the transaction).

This is why scripts are often referred to as validators. Unlike some other blockchain systems, they are also, therefore, fully deterministic. Their execution only depends on the transaction they're involved with, and evaluating the transaction's outcome is possible before sending it to the network. Datums act as local states, and redeemers are user inputs provided in the transaction itself.

If we take a step back and look at the typical public/private key procedure for spending funds, we can see how eUTxO is merely a generalization of that. Indeed, the public key (hash) can be seen as the datum, whereas the signature is the redeemer. The script is the digital signature verification algorithm that controls whether the signature is valid w.r.t the provided key.

Purposes

So far, we've mostly talked about scripts in the context of validating whether an output can be spent. We've also briefly mentioned earlier how scripts can be used to control the publication of delegation certificates or how consensus rewards can be withdrawn.

These different use cases are commonly referred to as script purposes. Until now, we've seen three purposes: spend, publish and withdraw. There's a fourth one: mint.

The mint purpose refers to scripts that are executed to validate whether user-defined assets can be minted (i.e. created) or burned (i.e. destroyed). Cardano indeed supports user-defined assets which are used to represent both fungible quantities (like a protocol currency) or non-fungible quantities (a.k.a NFTs).

The rules that govern the creation or destruction of an asset are defined as a script. We often refer to such scripts as minting policies, which correspond to the mint purpose above.

Each purpose, therefore, indicates for what purpose a script is being executed. During validation, that information is passed to the script alongside the transaction and the redeemer. Note that only scripts executed with the spend purpose are given a datum. This is because they can leverage the data payload present in outputs, unlike the other purposes that do not get this opportunity.

TL;DR

And we've reached the end of this crash course. Let's do a final recap regarding scripts, datums and redeemers.

  • Scripts are akin to parameterized predicate functions, returning either true or false.
  • Datums take the role of function parameters, whereas redeemers the one of argument.
  • Scripts are also called validators and are completely deterministic.
  • Scripts are used to validate specific operations in a transaction.
  • What a script is used for is referred to as its purpose. There are 4 purposes:
    • spend -- controls how to spend outputs;
    • publish -- controls how to publish delegation certificates;
    • withdraw -- controls how to withdraw consensus rewards;
    • mint -- controls how to mint or burn assets.
  • Only spending scripts (i.e. purpose=spend) have access to a datum.

Language Tour

This section covers the syntax of Opshin and in particular the available Python language features.

OpShin is Python

As a general disclaimer we would like to point out that OpShin is a restricted version of Python. We encourage you to always try to write code the way you would do it in a normal Python program. Play around to see what is accepted by the compiler and what not. If the compiler accepts your code, the code is safe to run (the only notable exception are type downcasts, covered later).

The main goal of OpShin is that you can express the smart contract logic the way you want - hence the choice of Python, which is designed to be simple and intuitive.

If you find yourself going to great lengths to satisfy the OpShin compiler, please do open an Issue at the GitHub repository. We are always looking for feedback to simplify the experience of coding with OpShin.

Structure

OpShin code is an imperative programming language. As such, code is written by instructing the computer to perform computations. Each line of code corresponds to one instruction. A simple instruction is the assignment.

# Assign value 2 to variable named a
a = 2

If we want to not only store but also read the value, we can print it like this.

# Assign value 2 to variable named a
a = 2
# print the content of variable a
print(a)

When executed locally, this will print 2 to the command line. When executed on chain, this code will append the string "2" to the on-chain debug logs - you can use this to inspect how your code is working!

Can you guess what the following program will print?

a = 2
b = 4
print(a + b)

Types

Every variable in OpShin has a type. In general we do not need to tell OpShin in advance what type variables has - it is able to derive the type on its own. In the above example, variable a has type int. This is the type for whole (integer) numbers. There are a few more types that are introduced in the following sections.

Note that types are important in OpShin. If types do not match at compilation time, the contract can not be compiled. This is to avoid unnecessary errors and prevent bugs in the code. For example the following is not allowed, since it is a non-sensical instruction:

a = 2 + "hello"

The left part of this addition has type int while the right part has type str (the text/"String" type). An addition between them is not unambiguous and generally does not make too much sense. Hence, when trying to compile this statement, you will face a compiler error.

You may see these errors a lot (but hopefully not too often). They are telling you in advance that an operation will not work as expected. Don't fret! Take these error messages as guidance on what works and what not and adjust your program as required by the compiler to obtain valid code.

It is not possible to change the type of your variable later. The following program is forbidden

a = 10
a = "hello"

Instead, we recommend simply changing the variable name if you want to change the type. This will also be less confusing to the reader!

Control flow

In OpShin, control flow can be introduced using statements like if.

if a == 2:
    print("a is equal to 2!")
    a = 10
print("hello!")

In this case, the expression a == 2 has a boolean type. It can evaluate to either True (a is indeed equal to 2) or False. If it is True, then the indented part is executed, so the program will print a is equal to 2! and afterwards assign the value 10 to a.

The unindented part will always be executed. Generally, indentation indicates that code belongs to a different "layer" of the program, i.e. an if/else statement, a loop, a function or a class.

Primitive Types

Opshin has 5 primitive types. These are called primitive as they are the basis of all other types and not composed of others.

  • int
  • bool
  • bytes
  • str
  • None

int

This is the only numeric type in Opshin. It represents integer numbers. Opshin's int type works just like the int type in Python and can be written in different notations. Note that all of the below examples evaluate to an integer and can be compared and added to each other!

# Opshin supports typical integer literals:
my_decimal = 17  # decimal notation
my_binary  = 0b10001  # binary notation
my_hex     = 0x11  # hexadecimal notation
my_octal   = 0o121 # octal notation

# What will this print?
print(my_decimal == my_hex)

Operation on integers

OpShin offers a number of builtin operations for integers.

# Addition
a = 5 + 2  # returns 7

# Subtraction
a = 5 - 2  # returns 3

# Multiplication
a = 5 * 2  # returns 10

# Integer division (floored)
a = 5 // 2  # returns 2

# Power
a = 5 ** 2  # returns 25

Proper Division is not supported because UPLC has not way to represent floating point numbers. If you want to perform operations on rational numbers, use the fractions library

bool

The bool type has two possible values: True or False. Control flow (if/else, while) are usually controlled using boolean types.

booly = False

Operation on Booleans

OpShin offers a number of builtin operations for booleans.

# Conjunction
b = True and False  # returns False

# Disjunction
b = True or False  # returns True

# Negation
b = not True  # returns False

# Cast to integer
b = int(True)  # returns 1 (False gives 0)

str

The str type in Opshin stores Strings, i.e. human-readable text. It is mostly used for printing debug messages.

stringy = "hello world"

not_so_secret_message = "..."

Operation on Strings

OpShin offers some builtin operations for strings.

# Concatentation
s = "hello " + "world!"  # Returns "hello world!"

# Cast to integer
s = int("42")  # returns 42

.encode()

str are usually stored in binary format in the so-called UTF-8 encoding. This is for example the case for native token names. The function encode transforms a normal, readable string into its binary representation.

"OpShin".encode()  # returns b"\x4f\x70\x53\x68\x69\x6e"

str()

If you want to convert anything into a string for debugging purposes, you may call the function str on it.

str(42)  # returns "42"

Note that print also implicitly calls str on its input before printing it for your convenience.

print(42)  # prints "42"

Format strings

More conveniently, if you want to combine strings and other values to a nicely formatted output, use formatting strings like this:

print(f"the value of a is {a}, but the value of b is {b}")

This will print the original string and substitute everything between { and } with the evaluated expression.

bytes

The bytes type in Opshin represents an array/string of bytes. It's usually called ByteArray or ByteString in other programming languages. You may use it to store raw binary data.

my_bytes = b"ooh a bytestring"

This type is usually used to represent hashes or CBOR. Note that bytestrings per default generate the bytestring for the ASCII character input. If you have a bytestring 0xaf2e221a represented in hexadecimal format, you can write it like this as a literal in OpShin.

hashy = b"\xaf\x2e\x22\x1a"

You may also use the helper function bytes.fromhex.

hashy = bytes.fromhex("af2e221a")

Operation on bytes (ByteStrings)

OpShin offers operations for bytestrings.

# Concatentation
s = b"hello " + b"world!"  # Returns b"hello world!"

[] - indexing and slicing

Python has a general concept of accessing elemtns at a specific index or accessing a slice of them. This also works for bytes. You can either access a single byte of the string.

b"test"[1]  # returns 101

Or you can access a substring of the bytes from the first (inclusive) to last (exclusive) indicated index using [a:b] slicing syntax.

b"test"[1:3]  # returns b"es"

In python, negative indices y indicate access at len(x) - y for object x. The following returns the byte at the last position of the byte string!

b"test"[-1]  # returns 116

This also works with slices.

.decode()

bytes may represent unicode UTF-8 encoded strings. This is for example the case for native token names. The function decode transforms a byte string into a normal, readable string.

b"\x4f\x70\x53\x68\x69\x6e".decode()  # returns "OpShin"

.hex()

bytes are better readable when displayed in hexadecimal notation. Use hex for this.

b"\x4f\x70\x53\x68\x69\x6e".hex()  # returns "4f705368696e"

len()

If you want to know the length of a bytestring, call len() on it.

len(b"OpShin")  # returns 6

None

The None type is exactly like the None type in Python. In other cardano smart contract languages it's called unit and denoted by empty brackets, (). It doesn't do anything and is usually used to denote the absence of a value.

null_val = None

Container Types

Opshin has two main container types:

  • List[a]
  • Dict[k, v]

Note: The container types are generic, and the letter in square brackets ([]) are the type arguments

List[a]

The Opshin List type is a container type that is used to hold multiple values of the same type. This is works just like the list type in Python.

listy: List[int] = [1, 2, 3, 4, 5]

Note: Opshin lists are actually linked-lists. This is because that's how lists are implemented in UPLC.

List Operations

You can add lists using the + operator.

print([1, 2, 3, 4] + [5, 6])
# prints "[1, 2, 3, 4, 5, 6]"

List Access

You may access elements at arbitrary positions within a list like this:

listy[3]  # will return the element at the 4th position (3rd with 0-based indexing), i.e. 4

If you want to count from the back, use negative numbers:

listy[-2]  # returns the second-to-last element, i.e. 4

List Slices

You may access slices of a list using the slicing syntax.

["a", "b", "c", "d"][1:3]  # returns ["b", "c"]

List Comprehension

Opshin supports Python's list comprehension syntax. This allows for very compact list initialization:

squares = [x**2 for x in listy]

len(x)

The len method returns the length of the list as an int:

lenny: int = len(listy) 
lenny == 5  # True

Membership using in

You can check whether some element is included in a list of elements using the keyword in.

4 in [1, 2, 3, 4, 5]  # True
100 in range(10)  # False

Empty lists

Empty lists may only be created in annotated assignments. This is to ensure that the type of the empty list can be correctly inferred.

a = []  # Fails! Unclear what type this expression has!

a: List[int] = [] # This works!

Dict[k, v]

The Dict type represents a map from keys of type k to values of type v. It works just like the dict type in Python.

# A dictionary storing scores in a game.
scores: Dict[str, int] = {"god_binder": 12, "radio_knight": 42}

Dictionary Access

A dictionary implements a map. In order to find the mapped value of element x we can access it in the dictionary via dict[x].

scores["god_binder"]  # returns 12

Take care when accessing values that are not contained in the dictionary - in case of missing keys, the contract will fail immediately.

scores["peter_pan"]  # fails with "KeyError"

If you are not sure whether a key maps to something in the dictionary use dict.get(x, d). It will try to return the value mapped to by x in the dictionary. If x is not present it will return d.

It is important that d is of the value type v to guarantee type safety.

scores.get("god_binder", 0)  # returns 12
scores.get("peter_pan", 0)  # returns 0

.keys()

The .keys() method returns a list of the keys in a dictionary,

players: List[str] = scores.keys() # ["god_binder", "radio_knight"]

.values()

The .values() method returns a list of all the values in a dictionary.

raw_scores: List[int] = scores.values() # [12, 42]

.items()

The .items() method returns a tuple of the each key-value pair in the dictionary. This is particularly useful if you want to iterate over all pairs contained in a dictionary.

for username, score in scores.items():
    print(f"{username} scored: {score}")
    # prints first "god_binder scored: 12" and then "radio_knight scored: 42"

Custom Classes

If you want to define a custom class to be used in Opshin, it must be a dataclass which inherits from the PlutusData class which can be imported from opshin.prelude.

from opshin.prelude import *

@dataclass()
class Person(PlutusData):
    # Every person has a UTF8 encoded name
    name: bytes
    # Every person has a year of birth
    birthyear: int

PlutusData may contain only bytes, int, dicts, lists or other dataclasses.

Note that str and None are not valid field types of PlutusData.

Constructing objects

You can construct an object by calling the classname with the variables in order defined in the class.

a = Person(b"Billy", 1970)

Attribute access

All named attributes defined in the class body are accessible by object.attribute. For example, to access the name of a person we would run

print(a.name)  # prints b"Billy"

Union types

It may happen that you allow more than a single type of data for your application (think of a Smart Contract that allows different operations on it). In this case, you may define a Union[A, B, ...] type. This expresses that a variable may be of either of the classes inside the square brackets.

@dataclass()
class Plant(PlutusData):
    CONSTR_ID = 1
    # Plants have no further properties

@dataclass()
class Animal(PlutusData):
    CONSTR_ID = 2
    # Animals have a name too!
    name: bytes
    # They also have an owner, which is another dataclass
    owner: Person


# Note all of these classes have distinct CONSTR_ID values
CityDweller = Union[Animal, Plant, Person]


# Both assignments are fine, because a is annotated
# to be of the Union type and can be of either class
c: CityDweller = Plant()
c = Animal(b"jackie", a)

Importantly, you need to set the CONSTR_ID of Classes that occur in a Union to distinct values. On-Chain, classes are only distinguished by their CONSTR_ID value. If omitted, the CONSTR_ID defaults to an almost-unique determinstic value based on the Class definition.

Type casts

If a variable is of an Union type we may still want to distinguish how we handle them based on the actual type. For this, we can use the function isinstance. isinstance(x, A) returns True if value x is an instance of class A (which is not a Union type!).

# this is forbidden!
# If a is a Plant or Animal, it does not have a birthyear so this operation will fail.
print(a.birthyear)

if isinstance(a, Person):
    # Here its okay
    # OpShin recognizes the isinstance check and knows that
    # a is of type Person in this branch of the condition
    print(a.birthyear)

We can combine isinstance calls and access shared attributes across classes.

if isinstance(a, Person) or isinstance(a, Animal):
    # a is of type Union[Person, Animal] in this branch
    # Both classes have the same attribute at the same position
    # so we can access it in either case
    print(a.name)

You can also form the complement of type casts.

a: Union[Person, Animal] = ...
if isinstance(a, Person):
    # a is of type Person in this branch
    print(a.birthyear)
else:
    # a is of type Animal in this branch
    print(a.owner)

Note that you can also use str / print directly to get a very informative representation of the object

print(a)
# "Person(name=b'Billy', birthyear=1970)"

.to_cbor()

To obtain the CBOR representation of an object, you may call its to_cbor method. This will return the bytes representing an object in CBOR.

print(a.to_cbor().hex())
# prints "d8799f4542696c6c791907b2ff"

Variables

Variables in Opshin are declared just like you'd expect them to in Python:

# A simple variable declaration
x = 5 

# A variable declaration with annotated type, x must be an integer
x: int = 5 

# Variables in Opshin can be mutated.
x += 1

Note: For now int is the only number type available in Opshin. fractions are coming soon.

Type annotation

If you want to make sure that a variable has the type you expect it to have you may use type annotation at the time of assinging a variable.

x: int = some_function()

This will make sure that some_function actually returns an integer.

Annotations can also be used for type up- and downcasts. For example if you receive a value y of type Union[A, B] you can run

z: A = y

This will cast y to type A in the type system but will not check the type during runtime. Use with care.

The other way around, if you receive a value of type A but you may want to use it as a Union[A, B] you can run

z: Union[A, B] = y

This will allow you to also store objects of type B in z later in the code.

Note that the type of a variable can not be changed after initialization. This is true as of version 0.19.0 and may change again in a later version.

Tuple Assignments

Opshin supports Python's tuple assignment syntax:

a, b = 3, 7

Conditional Statements

Opshin uses if, elif and else statements for conditional control flow. elif is a short form of else if and behaves like you would expect it.

if

n = 4
if n < 5:
    print("Less than 5.")
elif n == 5:
    print("Equal to 5.")
else:
    print("Greater than 5.")

pass

This statement does not do anything. It is mainly used to show the compiler that you respect an indent for an otherwise empty control flow branch.

if check:
    pass
else:
    some_important_function()

assert

The assert statement is an important tool in Smart Contracts to check that conditions of the contract are met. It checks that a statement is correct and otherwise immediately halts the contract with an error message if provided.

assert money_locked_in_contract >= 100000, f"Expected 100000 but only received {money_locked_in_contract}"

Semantically it can be imagined like this:

if condition_met:
    pass
else:
    print(error_message)
    <fails contract>

Informative error messages will save you plenty of time when executing and debugging your off-chain transactions.

Loop Statements

Opsin supports both while and for loops.

while loops

while loops in Opshin work just like in Python. They have a body and a boolean expression and the code in the body is looped while the boolean expression returns True.

count = 0

while count < 5:
    print("'count' is ", count, ".")
    count += 1

for loops

for loops are use for iterating over lists and other iterables. The loop through the elements of the iterable and executes the code in the block using the element from the list.

names = ["Ada", "Voltaire", "Basho"]
for name in names:
    print(name)

for i in range(10):
    ...

Dictionaries are also iterables and they can be iterated over using a for loop:

scores = {"Byron": 1200, "Vasil": 900, "Ada": 1790, "Shelley": 1400}

# Iterating over the keys and values using '.items()'
for name, score in scores.items():
    print(name + " scored: " + str(score))

# Iterating over the keys using '.keys()'
for name in scores.keys():
    ...

# Iterating over the values using '.values()'
for score in scores.values():
    ...

Functions

Regular Functions

Functions in Opshin should have type annotations for the arguments and the return value. Return statements can be placed anywhere inside a function. Note that all return statements must have a type compatible with the annotated return type.

def fibonacci(n: int) -> int:
    if n < 2:
        return 1
    else:
        return fibonacci(n-1) + fibonacci(n-2)

As shown, Opshin functions support recursion. A function foo is called as foo(x, y, ...) with arguments x, y, etc.

If the function does not have a return statement in some path, the implicit return value is None. This can be useful for code that has side-effects, such as assertion checks.

def check_valid(n: int) -> None:
  assert n > 0, f"Invalid negative int encountered: {n}"

If the type annotation is missing for any field, the implicit annotation is Any. This may be fine for your use case, but note that this is slightly less efficient (and less clear to the reader) than a properly type annotated function.

Note that you can define functions locally and within other functions, so that they do not clutter your namespace and can not be used in the wrong context. The following is legal.

def mul_twice(x: int) -> int:

   def mul_once(x: int) -> int:
        return x * x

   return mul_once(x) * mul_once(x)

print(mul_twice(5))
# "625"

Lambdas and list expressions

Generally OpShin does not support the use of lambda functions, as they don't allow specifying types in the function signature. Instead, consider using a local function definitions or list expressions. For example instead of the following expression

ys = map(lambda x: 2*x, filter(lambda x: x > 0, xs)) # does not work.

You could either define local functions like this

def greater_zero(x: int) -> bool:
  return x > 0

def mul_2(x: int) -> int:
  return 2*x

ys = map(mul_2, filter(greater_zero, xs)) # does not work.

Or you can express this very compactly and readable directly in a list expression like this

ys = [2*x for x in xs if x > 0] # works!

Builtins

A growing list of the Python builtin functions is being implemented in OpShin. It will be documented here.

all(x: List[bool]) -> bool

Returns whether all booleans in a list are True.

any(x: List[bool]) -> bool

Returns whether any of the booleans in a list is True.

abs(x: int) -> int

Returns the absolute value of an integer.

bytes(x: int) -> bytes

Returns a bytestring with 0s of length x.

bytes(x: List[int]) -> bytes

Returns a bytestring where every byte corresponds to one integers in x.

chr(x: int) -> str

Returns the character for unicode code point x.

hex(x: int) -> str

Returns the hexadecimal representation of an integer.

len(x: Union[bytes, List[Anything]]) -> int

Returns a the length of x for bytes and lists.

max(x: List[int]) -> int

Returns the maximum of all integers in a list.

min(x: List[int]) -> int

Returns the minimum of all integers in a list.

oct(x: int) -> str

Returns the octal representation of an integer.

pow(x: int, y: int) -> str

Returns xy

range(x: int) -> List[int]

Returns the integers from 0 to x-1 as a list.

reversed(x: List[Anything]) -> List[Anything]

Returns the reversal of list x.

str(x: Anything) -> str

Returns a stringified representation of x.

sum(x: List[int]) -> int

Returns the sum of all integers in a list.

🔗 Standard Library

This page will take you to the definition of the OpShin standard library in the Opshin package. Click here if the automatic redirect does not work.

Smart Contract Tour

This section will teach you how to write Cardano smart contracts in Opshin.

Validator Scripts

Congrats, if you've gotten to this section you now understand the basics of Opshin. Now we get to put those together to write smart contracts.

If you remember from the page on the EUTXO model. UTXOs are like bundles of money and a datum stored on the blockchain, they are locked by a validator script which is run when someone attempts to spend that UTXO. Of course, this is only the case for spending contracts i.e. Contract addresses that hold actual UTxOs. Minting Scripts and other types of validators do not have a datum as parameter.

Smart contract development on Cardano centers around writing validators in a way that allows only transactions that follow the business logic of the application. Every validator is a simple pure function that takes two to three arguments:

  1. Datum: This is data stored alongside the UTXO on the blockchain (in the case of Spending Validators i.e. Contract Addresses).
  2. Redeemer: This data included in the transaction that attempts to spend the UTXO.
  3. Script Context: This object stores information about the transaction attempting to spend the UTXO.

Note: Datum and Redeemer are entirely controlled by the user and may not be trusted, but the Script Context may be trusted as it is assembled by the node. Consequently, the Datum and the Redeemer can be of any type depending on the contract, but the Script Context is always of type ScriptContext.

A validator either does not fail or fails (i.e. through the use of assert or an out-of-index array access). If it does not fail then, independent of the returned value, the contract execution is counted as a success and the contract validates the transaction.

Note: Other Smart Contract languages return a boolean value that determines the success of the transaction. If you return False in OpShin, the contract will succeed in any case.

Example Validator - Gift Contract

In this simple example we'll write a gift contract that will allow a user create a gift UTXO that can be spent by:

  • The creator cancelling the gift and spending the UTXO. (1)
  • The recipient claiming the gift and spending the UTXO. (2)
# gift.py

# The Opshin prelude contains a lot useful types and functions 
from opshin.prelude import *


# Custom Datum
@dataclass()
class GiftDatum(PlutusData):
    # The public key hash of the gift creator.
    # Used for cancelling the gift and refunding the creator (1).
    creator_pubkeyhash: bytes

    # The public key hash of the gift recipient.
    # Used by the recipient for collecting the gift (2).
    recipient_pubkeyhash: bytes


def validator(datum: GiftDatum, redeemer: None, context: ScriptContext) -> None:
    # Check that we are indeed spending a UTxO
    assert isinstance(context.purpose, Spending), "Wrong type of script invocation"

    # Confirm the creator signed the transaction in scenario (1).
    creator_is_cancelling_gift = datum.creator_pubkeyhash in context.tx_info.signatories

    # Confirm the recipient signed the transaction in scenario (2).
    recipient_is_collecting_gift = datum.recipient_pubkeyhash in context.tx_info.signatories

    assert creator_is_cancelling_gift or recipient_is_collecting_gift, "Required signature missing"

This might be a bit to take in, especially the logic for checking the signatures. The most important part is that you see the parameters and the return type resp. the assert statements actually controlling the validation. In the next chapter we'll do a deep dive into the single most important object in Cardano smart contracts, the ScriptContext.

The ScriptContext

Majority of the logic of smart contracts has to do with making assertions about certain properties of the ScriptContext. It contains a lot of useful information such as:

  • When is the transaction?
  • What will the inputs of the transactions be?
  • What will the outputs of the transaction?

All of these things are contained in the ScriptContext object passed into the contract as the last argument. This section covers the most interesting parts of the Script Context - if you want to learn more check out the full documentation at the module description

ScriptContext

The ScriptContext is defined as:

@dataclass()
class ScriptContext(PlutusData):
    """
    Auxiliary information about the transaction and reason for invocation of the called script.
    """

    tx_info: TxInfo
    purpose: ScriptPurpose

The most important field in the ScriptContext is the tx_info field which is of type TxInfo.

TxInfo

@dataclass()
class TxInfo(PlutusData):
    """
    A complex agglomeration of everything that could be of interest to the executed script, regarding the transaction
    that invoked the script
    """

    # The input UTXOs of the transaction.
    inputs: List[TxInInfo]
    # The reference UTXOs of the transaction.
    reference_inputs: List[TxInInfo]
    # The output UTXOs created by the transaction.
    outputs: List[TxOut]
    # Transaction fee to be payed for the transaction.
    fee: Value
    # The value minted in the transaction.
    mint: Value
    dcert: List[DCert]
    wdrl: Dict[StakingCredential, int]
    valid_range: POSIXTimeRange
    # The signatures for the transaction.
    signatories: List[PubKeyHash]
    redeemers: Dict[ScriptPurpose, Redeemer]
    data: Dict[DatumHash, Datum]
    # The ID of the transaction.
    id: TxId

TxInInfo

This type contains data about a UTXO being used as a transaction input.

@dataclass()
class TxInInfo(PlutusData):
    """
    The plutus representation of an transaction output, that is consumed by the transaction.
    """

    out_ref: TxOutRef
    resolved: TxOut

TxOut

This type contains data about a UTXO.

@dataclass()
class TxOut(PlutusData):
    """
    The plutus representation of an transaction output, consisting of
    - address: address owning this output
    - value: tokens associated with this output
    - datum: datum associated with this output
    - reference_script: reference script associated with this output
    """

    address: Address
    value: Value
    datum: OutputDatum
    reference_script: Union[NoScriptHash, SomeScriptHash]

Handling Time

In a lot of contracts, time is of the essence. Properly handling time is essential for a lot of contracts like auctions and swaps.

The valid_range property in the ScriptContext specifies within what time the transaction can be submitted, it's used to constrain the time of the transaction. It's of type POSIXTimeRange.

POSIXTimeRange

The POSIXTimeRange type is used to specify a time range.

dataclass()
class POSIXTimeRange(PlutusData):
    """
    Time range in which this transaction can be executed
    """

    # Lower bound for the execution of the transaction
    # Can be negative infinity or a int.
    lower_bound: LowerBoundPOSIXTime
    # Upper bound for the execution of the transaction
    # Can be positive infinity or a int.
    upper_bound: UpperBoundPOSIXTime

A POSIXTimeRange can be visualized like a subsection of a numberline starting from negative infinity(-∞) to positive infinity (+∞)

         ⬤-------------→⬤
-∞       -3 -2  -1   0   1   2   3       +∞
 └┴┴┴┴┴─┴─┴──┴───┴───┴───┴───┴───┴─┴─┴┴┴┴┘ 

Useful Methods

Helper functions for the POSIXTimeRange type are stored in opshin.ledger.interval.

make_range(lower_bound: int, upper_bound: int) -> POSIXTimeRange

This takes a the lower bound as the first argument and the upper bound as the second argument makes a POSIXTimeRange that starts from lower_bound and ends at upper_bound.

valid_range = make_range(-3, 2)

This can be visualized as:

     lower_bound        upper_bound
          ⬤----------------→⬤
-∞       -3 -2  -1   0   1   2   3       +∞
 └┴┴┴┴┴─┴─┴──┴───┴───┴───┴───┴───┴─┴─┴┴┴┴┘ 

make_from(lower_bound: int) -> POSIXTimeRange

This takes a the lower bound as the first argument and makes a POSIXTimeRange that starts from lower_bound and ends at positive infinity.

valid_range = make_from(-1)

This can be visualized as:

            lower_bound     
                 ⬤---------------------→⬤
-∞       -3 -2  -1   0   1   2   3       +∞
 └┴┴┴┴┴─┴─┴──┴───┴───┴───┴───┴───┴─┴─┴┴┴┴┘ 

make_to(upper_bound: int) -> POSIXTimeRange

This takes a the upper bound as the first argument and makes a POSIXTimeRange that starts from negative infinity and ends at upper_bound.

valid_range = make_to(1)

This can be visualized as:

                    upper_bound 
 ⬤---------------------→⬤
-∞       -3 -2  -1   0   1   2   3       +∞
 └┴┴┴┴┴─┴─┴──┴───┴───┴───┴───┴───┴─┴─┴┴┴┴┘ 

Vesting Contract

To really grok how time is managed on Cardano we'll write a simple vesting contract.

Advanced Topics

This section contains a few selected, advanced use cases of OpShin.

Constant Folding

OpShin supports the command-line flag --constant-folding or short --cf. With this flag, every expression is evaluated at compile time by the python eval command. On one hand this enables the precomputation of expensive constants in your code. Specifically it evaluates all expressions that invoke only constants or variables that are declared exactly once in the contract. print statements are not pre-evaluated. For example, the following expressions would be folded at compile time:

0 == 1  # evaluates to False

@dataclass
class A(PlutusData):
    a: int
    b: bytes

A(0, b"") # evaluates to the object

def foo(x):
    return x

foo(0) # evaluates to 0

bar = 2
bar = 1
bar + 1  # is not evaluated at compile time

Forcing three parameters

By setting the flag --force-three-params you can enable the contract to act with any script purpose (i.e. minting, spending, certification and withdrawal).

When a script invoked with a minting, certificaton or withdrawal purpose, the validator function is called such that the first parameter of the contract (the datum) is set to Nothing() (a PlutusData object with no fields and constructor id 6). Therefore, the compiler enforces a union type that includes Nothing as an option when using the three parameter force flag.

An example of a script that acts as both minting and spending validator can be found in the wrapped_token example script.

Checking the integrity of objects

OpShin never checks that an object adheres to the structure that is declared for its parameters. The simple reason is that this is costly and usually not necessary. Most of the time an equality comparison (==) between PlutusData objects is sufficient (and fast).

There are cases where you want to ensure the integrity of a datum however. For example in an AMM setting where the pool datum will be re-used and potentially re-written in subsequent calls. In order to prevent malicious actors from making the datum too big to fit in subsequent transactions or to prevent them from writing values of invalid types into the object, you may use check_integrity.

An example use case is found below. This contract will fail if anything but a PlutusData object with constructor id 2 and two fields, first integer and second bytes, is passed as a datum into the contract.

Note: Yes, this implies that without the explicit check the contract may pass with whatever type d is, since its field or constructor id are never explicitly accessed.

from opshin.prelude import *
from opshin.std.integrity import check_integrity

@dataclass
class A(PlutusData)
    CONSTR_ID = 2
    a: int
    b: bytes

def validator(d: A, r: int, c: ScriptContext):
    check_integrity(d)

🔗 Ledger definitions

This page will take you to the definition of the Script Context of the Cardano Ledger in the OpShin package. Click here if the automatic redirect does not work.

🔗 Examples

This page will take you to the examples section in the awesome-opshin repository, featuring many example projects. Click here if the automatic redirect does not work.

Common Issues when interacting with OpShin

RecursionError: maximum recursion depth exceeded

This may happen when your contract is too large. Try setting the flag --recursion-limit to something high like 2000. If it does not go away even with values above i.e. 10000, please open an issue.

Getting Started with Opshin

If you want to learn more about OpShin you can find additional resources at these points.

OpShin Pioneer Program

Check out the opshin-pioneer-program for a host of educational example contracts, test cases and off-chain code.

Example repository

Check out the opshin-starter-kit repository for a quick start in setting up a development environment and compiling some sample contracts yourself.

You can replace the contracts in your local copy of the repository with code from the examples section here to start exploring different contracts.

Awesome Opshin

The "Awesome Opshin" repository contains a host of DApps, Tutorials and other resources written in OpShin or suitable for learning it. It can be found in the awesome-opshin repository.

Developer Community and Questions

This repository contains a discussions page. Feel free to open up a new discussion with questions regarding development using opshin and using certain features. Others may be able to help you and will also benefit from the previously shared questions.

Check out the community here

You can also chat with other developers in the welcoming discord community of OpShin

Help us improve OpShin by participating in this survey!

A short guide on Writing a Smart Contract

A short non-complete introduction in starting to write smart contracts follows.

  1. Make sure you understand EUTxOs, Addresses, Validators etc on Cardano. There is a wonderful crashcourse by @KtorZ. The contract will work on these concepts
  2. Make sure you understand python. opshin works like python and uses python. There are tons of tutorials for python, choose what suits you best.
  3. Make sure your contract is valid python and the types check out. Write simple contracts first and run them using opshin eval to get a feeling for how they work.
  4. Make sure your contract is valid opshin code. Run opshin compile and look at the compiler erros for guidance along what works and doesn't work and why.
  5. Dig into the examples to understand common patterns. Check out the prelude for understanding how the Script Context is structured and how complex datums are defined.
  6. Check out the sample repository to find a sample setup for developing your own contract.

In summary, a smart contract in opshin is defined by the function validator in your contract file. The function validates that a specific value can be spent, minted, burned, withdrawn etc, depending on where it is invoked/used as a credential. If the function fails (i.e. raises an error of any kind such as a KeyError or AssertionError) the validation is denied, and the funds can not be spent, minted, burned etc.

There is a subtle difference here in comparison to most other Smart Contract languages. In opshin a validator may return anything (in particular also False) - as long as it does not fail, the execution is considered valid. This is more similar to how contracts in Solidity always pass, unless they run out of gas or hit an error. So make sure to assert what you want to ensure to hold for validation!

A simple contract called the "Gift Contract" verifies that only specific wallets can withdraw money. They are authenticated by a signature. If you don't understand what a pubkeyhash is and how this validates anything, check out this gentle introduction into Cardanos EUTxO. Also see the tutorial by pycardano for explanations on what each of the parameters to the validator means and how to build transactions with the contract.

Minting policies expect only a redeemer and script context as argument. Check out the Architecture guide for details on how to write double functioning contracts. The examples folder contains more examples. Also check out the opshin-pioneer-program and opshin-starter-kit repo.

The small print

Not every valid python program is a valid smart contract. Not all language features of python will or can be supported. The reasons are mainly of practical nature (i.e. we can't infer types when functions like eval are allowed). Specifically, only a pure subset of python is allowed. Further, only immutable objects may be generated.

For your program to be accepted, make sure to only make use of language constructs supported by the compiler. You will be notified of which constructs are not supported when trying to compile.

You can also make use of the built-in linting command and check it for example with the following command:

opshin lint spending examples/smart_contracts/assert_sum.py

Name

Eopsin (Korean: 업신; Hanja: 業神) is the goddess of the storage and wealth in Korean mythology and shamanism. [...] Eopsin was believed to be a pitch-black snake that had ears. [1]

Since this project tries to merge Python (a large serpent) and Pluto/Plutus (Greek wealth gods), the name appears fitting. The name e_opsin is pronounced op-shin. e