Skip to main content

Blueprint development guidelines

Introduction

This article is the primary reference material to consult when developing a blueprint. It provides guidelines for its design and implementation, and describes the prescribed rules for blueprint code. Now, to understand the overall mechanics of blueprints, see Nano contracts: how it works.

warning

This article refers to the following version of the Blueprint SDK: branch experimental/nano-testnet-v1.8.0 of hathor-core repository at GitHub.

Note that the Blueprint SDK is an experimental phase and evolving rapidly. Newer versions may include features not yet covered here. Moreover, backward compatibility is not guaranteed, and they may change the behavior described in this article.

warning

This article is in final review stage. While most of its content is accurate, some errors may still be present.

Python SDK

Hathor provides an SDK for blueprint development in Python 3. In the future, blueprint development may be expanded to other languages. For now, you need to develop your blueprint using Python 3.

Throughout this article, we will present the complete set of constraints that your blueprint code must follow. That is, your code must comply with all these rules to execute without errors on Hathor engine. Aside from these constraints, you are free to write Python 3 code as usual.

Module

A blueprint must be implemented as a single Python module — namely, a single file my_blueprint.py.

Imports

You can only import names explicitly allowed by the blueprint SDK. The following snippet provides an exhaustive list of allowed imports:

imports_you_can_use.py
# Standard and related third parties
from math import ceil, floor
from typing import NamedTuple, Optional, TypeAlias

# Hathor (local application/library)
from hathor.nanocontracts.blueprint import Blueprint
from hathor.nanocontracts.context import Context
from hathor.nanocontracts.exception import NCFail
from hathor.nanocontracts.types import (
Address,
Amount,
BlueprintId,
ContractId,
NCAction,
NCActionType,
public,
SignedData,
Timestamp,
TokenUid,
TxOutputScript,
VertexId,
view,
)

Any import outside this list is not allowed. For the hathor.nanocontracts package interface documentation, see Blueprint SDK — API.

Classes

Regarding classes, you must comply with the following rules:

  1. Your module must have exactly one class that inherits from Blueprint. This is the primary class of your module, that models contracts and is used to instantiate them.
  2. Like any regular Python module, your module may contain multiple other classes. However, these classes should only be used to instantiate temporary objects, such as error specifications.
  3. You must indicate to Hathor engine which is your blueprint class. Suppose MyBlueprint is that class:

The following snippet presents an example of on-chain blueprint module that comply with these rules:

my_blueprint.py
# Start with the imports.
# Remember: only allowed imports.
from hathor.nanocontracts.blueprint import Blueprint

...

# Define any ancillary classes:
class InvalidToken(NCFail):
pass

...

# Define the blueprint class.
# This is the class that will be used to instantiate contracts.
# We suggest you to use the same name as the module:
class MyBlueprint(Blueprint):

...

# Finally, assign the primary class to Hathor protocol:
__blueprint__ = MyBlueprint

Type annotations

There are two rules you must comply with:

  1. Type annotations are mandatory for all contract attributes, and methods' parameters and return values. The only exception is the first parameter of each method, conventionally self.
  2. When following rule (1), all generic types must be fully parameterized.

The following snippet provides examples of how to apply rule (1) in a simple case without generic types:

my_blueprint.py
...
class MyBlueprint(Blueprint):
"""Docstring explaining the blueprint.
After this docstring, you must specify all contract attributes.
Do this using type annotations.
"""

# Maximum value for loan
max_loan_value: int

# Id of the borrower
borrower_id: str

# Token that your contract operate with
token_uid: TokenUid

# Amount to be borrowed
amount: Amount

# Then, start defining the methods

# Example of method signature:
# Note that all parameters (except self)
# and the return value all use type annotations.
@view
def check_credit(self, credit_id: int, borrower_name: str) -> bool:
...
...

The next subsection describes how to apply rule (2).

Generic types

First, let's look at an example that is valid in Python but not sufficient for blueprints. The following snippet presents examples of incorrect contract attribute specifications:

incorrect_attribute_specification.py
# Let's specify contract attributes:

incorrect_list: list

other_incorrect_list: list[list]

incorrect_dict: dict

# What is wrong here?
# Generic containers are not fully parameterized!

What all these examples have in common — and what makes them incorrect in blueprints — is that they are not fully parameterized, and therefore do not comply with rule (2). The following snippet presents the same examples, now modified to be correct in blueprints:

fixed_attribute_specification.py
# Let's try to specify contract attributes again:
correct_list: list[str]

other_correct_list: list[list[int]]

correct_dict: dict[bytes, bool]

# Now all generic containers are fully parameterized!
# All good now!

The following snippet provides an exhaustive list of types that shall comply with rule (2):

generic_container_types.py
"""Generic containers:
Built-in types:
- list[T]
- set[T]
- dict[K, T]
- tuple[T, K, ...] (variable length)

hathor.nanocontracts package types:
- SignedData[T]
"""

These are all generic containers, and whenever you use these them in attributes, parameters, or return values, you must fully parameterize them.

Finally, note that you can nest multiple generic containers, as long as each is fully parameterized. The following code snippet presents examples of both correct and incorrect contract attribute specifications using nested generic container:

my_blueprint.py
...
class MyBlueprint(Blueprint):
"""Docstring explaining the blueprint.
After this docstring, you must specify all contract attributes.
Do this using type annotations.
"""

# this is correct
correct_attribute: list[str]

# this is NOT correct
incorrect_attribute: list

# this is a correct dict
another_correct: dict[Address, Amount]

# another_correct tuple
also_another_correct: tuple[str, int]

# tuple can have variable size as long as all items are the same
tuple_with_variable_size: tuple[str, ...]

# You can nest containers
nested_attribute: list[set[str]]

# You can nest more; just make sure fully parameterize.
more_nested_and_correct: dict[tuple[Address, str], Amount]

# This is also valid, to understand see the blueprint SDK API reference
last_correct: SignedData[str]

# Remember, these are all NOT correct:
incorrect_dict: dict
incorrect_set: set

# Then, start defining the methods
...

Forbidden names

The following snippet provides an exhaustive list of names that are not allowed for use:

forbidden_names.py
"""
Forbiden names List:
- __builtins__
- __import__
- compile
- delattr
- dir
- eval
- exec
- getattr
- globals
- hasattr
- input
- locals
- open
- setattr
- vars
"""

Reserved names

The following snippet provides an exhaustive list of names reserved by the SDK. You must not bind these names to any value or object:

SDK_reserved_names.py
"""
You cannot OVERRIDE these names:
- rng
- get_nanocontract_id
- get_balance
- call_public_method
- call_view_method
"""

In other words, you cannot override the SDK reserved names through function, class, or variable definitions; attribute assignments; method declarations; parameter names; or import statements. For example:

rng = 123            # NOT ALLOWED
def rng(): pass # NOT ALLOWED
self.rng = 123 # NOT ALLOWED

However, you may (and should) still use (access) these names via 'dot notation'. For example:

self.rng                    # ALLOWED
self.call_public_method() # ALLOWED

Attributes

In Python, data attributes are not declared prior to being assigned a value. Also, data attributes do not have types — only the values assigned to them do. However, the blueprint SDK mandates specifying each contract attribute along with its value type using type annotations. From the developer's perspective, it is as declaring variables in a statically‑typed language without type inference, such as C.

As a result, within your blueprint class, you must use type annotations to specify all contract attributes along with their respective value types. These type annotations shall appear before any method definitions. To know how to do this, see section Type annotations.

Allowed value types

Contract attributes must have values of types explicitly allowed by the blueprint SDK. The following snippet provides an exhaustive list of the allowed types:

allowed_attribute_value_types.py
"""Attribute data types you can use:
Built-in types:
- int
- str
- float
- bool
- bytes
- list[T]
- set[T]
- dict[K, T]
- tuple[T, K, V, ...] (variable length)

Standard library types:
- NamedTuple

hathor.nanocontracts package types:
- Address
- Amount
- BlueprintId
- ContractId
- Script
- SignedData[T]
- Timestamp
- TokenUid
- VertexId
"""

Any attribute value with a type not included in this list will not be allowed. For the data types provided by hathor.nanocontracts, see Blueprint SDK — API.

info

Note that, for now, instances of classes defined within the blueprint module itself cannot be used as attribute values.

Class attributes

Blueprints do not support class attributes. In object-oriented programming, such as in Python, classes can have both class and instance attributes. Instance attributes hold individual values for each object instantiated from the class, whereas class attributes share values across all instances.

Although Python allows for class attributes, they are not allowed by the blueprint SDK. As a result, nano contracts instantiated from the same blueprint do not share any dynamic values; they only share static, hard-coded values from the blueprint's source code.

Methods

When to use each type of method?

  • Define the public method initialize for contract creation.
  • Define other public methods for contract execution, providing the contract's functionalities to users.
  • Define view methods to implement logic that can be used both internally by other methods and externally by users.
  • Define internal methods to implement logic that can be used only by other methods.

How to define the type of a method?

Methods are designated as public, view, or internal by the usage of decorators. Public methods must be marked with the @public decorator, as seen in the bet method of the bet blueprint:

@public
def bet(self, ctx: Context, address: Address, score: str) -> None:
"""Make a bet."""
...

View methods must be marked with the @view decorator, as seen in the get_max_withdrawal method of the bet blueprint:

@view
def get_max_withdrawal(self, address: Address) -> int:
"""Return the maximum amount available for withdrawal."""
...

Any method not marked as either @public or @view is internal, as seen in the _get_action method of the bet blueprint:

def _get_action(self, ctx: Context) -> NCAction:
"""Return the only action available; fails otherwise."""
...
note

The underscore at the beginning of the internal method name is optional and is used here as a good practice in Python programming to indicate internal methods.

Finally, a method shall not be marked as both @public and @view.

info

To reiterate:

  • @public: public method.
  • @view: view method.
  • No decorator: internal method.
  • @public and view decorators together: error.

How can each method be called?

Public methods can be called externally by users via nano contracts transactions, and by other contracts executing; and internally by other public methods. Public methods can call any other method of the contract.

View methods can be called externally by users via full node API requests, and internally by any other method. View methods can call other view methods, cannot call public methods, and can call internal methods as long as these do not change the attributes of the contract.

Internal methods can only be called internally by any other method. They cannot be called externally by users. Internal methods can call other internal methods and view methods, and cannot call public methods.

tip

Be careful while implementing state changes within internal methods. This will work fine as long as the method is not used, directly or indirectly, by a view method.

The golden rule is that the primary call dictates if the state of a contract can or cannot be changed. If the first method called in a contract is public, the internal methods subsequently called can alter the contract’s attributes. However, if the first method is a view, the internal methods called must not alter the state of the contract. If they do, the call will raise an exception.

The following snippet presents an example of valid calls between methods in a blueprint:

class FooBar(Blueprint):
...

@public
def initialize(self, ctx: Context, *args) -> None:
...
# Some attribute of this blueprint
self.dummy = 0
...

@public
def foobar(self, ctx: Context, *args) -> None:
...
# foobar is public and therefore can call any other method:
self.foo(ctx)
self.bar()
self._grok()
self._qux()
...

@public
def foo(self, ctx: Context) -> None:
...

@view
def bar(self) -> int:
...
# Can call the internal methods
# as long as they don't change attributes
self._qux()

# No decorator, it's an internal method
def _grok(self) -> str:
...
# Be careful!
# This method changes attributes;
# shall not be called by view methods
self.dummy += 1
...

# Another internal method
def _qux(self) -> bool:
...
# This method doesn't change attributes;
# can be safely called by view methods
...

In a nutshell, when the primary call is a view method, you need to ensure that no method in the call chain tries to alter the contract’s attributes. Typically, you will have public methods providing the contract’s functionalities, view methods for user queries, and internal methods as helpers.

What can each method do?

No method can directly alter the contract's balance. The contract's balance is controlled by Hathor engine itself and is only updated after a public method completes successfully without throwing an exception, thereby authorizing all proposed deposits and withdrawals.

Regarding attributes, public methods can change them, and internal methods can also change them as long as they are never part of a chain call originated by a view method; because view methods cannot change attributes. The only effect of a view method should be compute and return a value.

Parameters

Regarding parameters, you must comply with the following rules:

  1. The first parameter of all methods must be the contract instance self-reference (conventionally self).
  2. The second parameter of all public methods must be a Context object.
  3. After the mandatory parameters defined in rules (1) and (2), methods can have any number of specific parameters.
  4. All parameters must follow the type annotation rules described in section Type annotations.
  5. The specific parameters of internal methods can have any valid type within the blueprint.
  6. However, the specific parameters of public and view methods may only have explicitly allowed types. The following snippet provides an exhaustive list of these allowed types:
allowed_parameters_types.py
"""Allowed types for parameters:
Built-in types:
- int
- str
- float
- bool
- bytes
- list[T]
- dict[K, T]
- tuple[T, K, V, ...] (variable length)

Standard library types:
- NamedTuple

hathor.nanocontracts package types:
- Address
- Amount
- SignedData[T]
- Timestamp
- TokenUid
- VertexId
"""

To comply with rule (4), see section Type annotations. For the other rules, the next subsections explain how to apply them to each method type.

Public methods

Public methods always have at least two parameters. (1) Since this is Python, the first parameter must be a self-reference, namely the contract itself — conventionally referred to as self. (2) The second parameter must always be a Context object. (3) After that, you can define any number of additional parameters. For example:

@public
def bet(self, ctx: Context, address: Address, score: str) -> None:
"""Make a bet."""
...

The specific parameters of the method may only use types explicitly allowed, as listed in rule (6) in the previous subsection.

View methods

View methods must have self as the first parameter and may have any number of additional parameters. For example:

@view
def get_max_withdrawal(self, address: Address) -> int:
"""Return the maximum amount available for withdrawal."""
...

Again, the specific parameters of the method may only use types explicitly allowed, as listed in rule (6) in the previous subsection.

Internal methods

Internal methods must have the first parameter self and may have any number of additional parameters. However, unlike public and view methods, internal methods can have parameters of any valid type within the blueprint. For example:

def _assess_credit(self, address: Address) -> bool:
"""Return if credit should or not be provided to given address."""
...

Return value

Regarding return values, you must comply with the following rules:

  1. All methods must include an explicit return type, following the type annotation rules described in section Type annotations.
  2. Public methods shall return None upon success or raise an exception.
  3. Internal methods may return any valid type within the blueprint.
  4. View methods may only return values with explicitly allowed types. The following snippet provides an exhaustive list of these allowed types:
allowed_return_types.py
"""Allowed types for view method's return values:
Built-in types:
- int
- str
- float
- bool
- bytes
- list[T]
- dict[K, T]
- tuple[T, K, V, ...] (variable length)

Standard library types:
- NamedTuple

hathor.nanocontracts package types:
- Address
- Amount
- SignedData[T]
- Timestamp
- TokenUid
- VertexId
"""

To comply with rule (1), see section Type annotations. For the other rules, the next subsections explain how to apply them to each method type.

Public methods

Public methods should execute to the end and return nothing (None) to indicate successful execution and raise an exception to indicate a failed execution. Take, for example, the swap demo blueprint available in the SDK. In the snippet below, we see in the method signature its return and in its body an exception being raised:

@public
def swap(self, ctx: Context) -> None:
"""Execute a token swap."""
if set(ctx.actions.keys()) != {self.token_a, self.token_b}:
raise InvalidTokens
...

Note that the return value None is explicitly indicated using the type annotation syntax -> None.

View methods

View methods should always return a value other than None, since the return value is their only effect. For example:

@view
def has_result(self) -> bool:
"""Return True if the final result has already been set."""
return bool(self.final_result is not None)

Note that the return value is explicitly indicated using the type annotation syntax ->.

Internal methods

Internal methods can return any valid type within the blueprint class. This includes, for example, other classes defined in the module. For example:

my_blueprint.py
...
class Foo:
pass
...
class MyBlueprint(Blueprint):
...
def bar(self) -> Foo:
...

Note that this internal method returns an instance of a class defined within the blueprint module itself, which is not possible in other types of methods.

Pre-invocation validations

Hathor protocol performs a series of validations before invoking a public method of a blueprint. If an NC (nano contract) transaction requests a withdrawal greater than the contract's funds, Hathor protocol ensures the method will not be called. Therefore, you don't need to worry about checking insufficient funds for withdrawals. For example, suppose Alice wants to withdraw 10 tokens A, but there are no tokens A in the contract's balance. In this case, Hathor protocol discards the transaction without even invoking the method.

Hathor protocol always passes a set of actions, where each deposit and withdrawal action refers to a distinct token. The actions object is a dictionary of NCAction, where each key is a token UID, and each value is an NCAction. Therefore, each existing token on Hathor platform can only appear once in actions. For example, this is a set of actions you might receive in the actions dictionary:

actions:
<token_A_UID>:
type: DEPOSIT
token_uid: <token_A_UID>
amount: 100
<token_B_UID>:
type: WITHDRAWAL
token_uid: <token_B_UID>
amount: 50
<token_C_UID>:
type: DEPOSIT
token_uid: <token_C_UID>
amount: 10
<token_D_UID>:
type: WITHDRAWAL
token_uid: <token_D_UID>
amount: 30

And here is a set of actions that you will never receive as argument in your method calls, where the same token appears more than once

actions:
<token_A_UID>:
type: DEPOSIT
token_uid: <token_A_UID>
amount: 100
<token_A_UID>:
type: DEPOSIT
token_uid: <token_A_UID>
amount: 50
<token_B_UID>:
type: DEPOSIT
token_uid: <token_B_UID>
amount: 10
<token_B_UID>:
type: WITHDRAWAL
token_uid: <token_B_UID>
amount: 30

Balances

The state of a contract comprises its attributes and its balances. The latter is also referred to as its multi-token balance. Attributes are defined in the blueprint code and are directly controlled by the contract through the logic of its public methods. The multi-token balance is controlled by Hathor engine, similarly to how the Ether balance of a contract is controlled on Ethereum platform. However, whereas in Ethereum all other tokens are just regular attributes, in Hathor all tokens are controlled by Hathor engine.

Reading

Since it is not a contract attribute, the procedure for reading its own balance is the same as reading any global state variable from the ledger (blockchain). In other words, it follows the same procedure as reading the multi-token balance of any other contract. To do this, an external interaction is required. To know what external interactions are and how to use them, see section External interactions.

Note that Hathor engine does not know which tokens a contract can receive deposits from. From its point of view, a contract can have a non-zero balance of any fungible and non-fungible token registered on the ledger (blockchain).

Additionally, Hathor engine is also not capable of listing and informing the contract which tokens it has a non-zero balance of. Therefore, the contract needs to know in advance which token it wants to check the balance of. That is, it cannot discover at runtime which tokens are present in its multi-token balance. As a result, you should define attributes to store which tokens the contract can receive deposits from, and among those, which ones it has already received.

Updating

Public methods do not directly change the contract balance. Instead, they authorize or deny the whole set of deposit and withdrawal requests made by users in NC transactions. Once authorized, it is Hathor protocol that performs the deposits and withdrawals (in the resolution phase). As a result, you cannot have a statement in a public method that modifies the contract balance.

Public methods cannot create deposit actions. Obviously, a contract cannot create a deposit in itself. Only a user can decide to make a deposit to a contract.

Public methods cannot create withdrawal actions. For example, suppose you want to add logic to a public method that, at runtime, creates a withdrawal action of 10 tokens A to be sent to any address. Can this be done? No.

To reiterate, public methods cannot define fund transfers from their balance to an address at runtime. The logic of a public method should only update attributes and authorize or deny the whole set of deposits and withdrawals in the NC transaction that called it.

You should take this into account when designing your blueprint. For example, suppose you want to create a blueprint that models the use case of "collectible trading cards" (e.g., Pokémon TCG, baseball cards, Magic the Gathering) sold in "blind boxes." Let's see the requirements for this type of use case:

  • The use case comprises a collection of trading cards.
  • These cards are sold in "blind boxes," each containing, for example, 5 cards.
  • The contents of each blind box are hidden, random, and only revealed after being opened, containing any of the 5 cards in the collection.
  • The trading card will be modeled as a collection of NFTs, where each card is an NFT.
  • The contract will hold a supply of NFTs in its balance, to be used as a stock for the blind boxes.
  • The user interacts with the contract to purchase a blind box.
  • Each blind box costs, for example, 10 tokens A.
  • The contract is responsible for generating and selling these blind boxes.
  • When called to execute the sale of a blind box to a user, the contract should generate, randomly and at runtime, the user's blind box using the NFTs in its balance, which serves as its stock.

At first, you might think of modeling the sale of blind boxes in this blueprint as follows:

  • A user creates an NC transaction calling the buy_blind_box method and sending only a deposit of 10 tokens A, with no arguments in args.
  • buy_blind_box verifies that the deposit equates to the purchase of exactly 1 blind box, priced at 10 tokens A.
  • buy_blind_box randomly selects 5 NFTs from those available in its balance.
  • buy_blind_box sends the 5 NFTs to the caller's address.

However, as we've seen, it is not possible for the contract to decide to send funds at runtime. All fund transfers occur through withdrawals and must be previously requested in the calling NC transaction. So how can this use case be modeled, given that the user cannot know in advance which NFTs can be withdrawn from the contract?

To model this type of use case, two contract executions will always be necessary:

  1. The first will request the purchase and generates the product.
  2. The second will request to collect the purchased product.

For our collectible trading cards blueprint, we could model it as follows:

  • A public method buy_blind_box that receives purchase orders through a deposit, randomly generates the blind box, and then saves in the contract's state that the buyer's calling address is entitled to collect the 5 NFTs selected in the blind box.
  • A view method reveal_blind_box that the user will use to discover which NFTs they can withdraw. This would be the real life equivalent to opening the physical blind box package and looking at the cards.
  • A public method get_blind_box that the user will use to withdraw the 5 NFTs contained in the blind box they purchased from the contract.

Business logic

Design each public method so that they implement business rules in order to:

  • Authorize or deny the whole set of actions present in the calling NC transaction, based on its current state, the user passed arguments and the Context object.
  • Update the contract's attributes. This update may or may not include records that fund transfers (i.e., withdrawals) are available for given addresses.
  • However, it cannot provoke the send of funds by itself. There needs to be a specific method for this, like get_blind_box and new NC transactions requesting such transfers in the form of withdrawals from the contract.

For example, in the case of collectible trading cards mentioned in the previous section, the public method buy_blind_box should:

  • Verify that actions contains only one deposit action with a value of 10 tokens A (the sale price of the blind box).
  • Randomly select 5 of these NFTs from the universe of NFTs available in its balance.
  • Finally, update its attributes to record that the buyer, represented by the calling address (in the NC transaction), is entitled to withdraw the 5 selected NFTs.

The same applies to the public method get_blind_box:

  • Verify if actions contains exactly 5 actions, one to withdraw each NFT revealed in the blind box.
  • Verify if the calling address (in the NC transaction) is entitled to withdraw the 5 requested NFTs.
  • Update its attributes to record that the buyer has already collected their blind box.
  • End its execution by returning nothing (None), signaling to Hathor protocol that authorizes the withdrawals.

When designing your blueprint, remember that there are two reasons why users execute a nano contract: The first and most common (1) is when the user wants to utilize a functionality of the contract, which will most often result in a set of deposits and withdrawals that they can make to/from the contract. For example, a bet blueprint has the public method bet, which users use to place bets on a bet contract.

The second reason (2) is when a special type of user, called an oracle, wants to provide off-chain data that a contract needs to operate. Still using the bet blueprint as an example, it has the public method set_result to be used by the oracle to report the result of a betting event (e.g., a football match, an MMA fight, etc.).

External interactions

External interactions refer to any interaction made by a contract with Hathor engine. That is, any statement in the blueprint code that is not self-contained within its own module. For example, manipulating its own attributes, instantiating variables, and calling its own methods are part of the contract's internal logic. On the other hand, read and write requests to the ledger (blockchain) and inter-contracts calls are all external interactions. In summary, external interactions in Hathor encompass the combination of environmental reads and external calls in the EVM model.

The Blueprint class provides methods for performing all possible external interactions. And since every blueprint inherits from the Blueprint class, these methods are part of the common behavior inherited by all blueprints.

Currently, the blueprint SDK allows the following external interactions:

  • Reading the contract’s own ID.
  • Reading the multi-token balance of any contract (its own or another).
  • Calling a view method of another contract.
  • Calling a public method of another contract.
  • Creating another contract.
  • Obtaining an RNG (random number generator).

To know how to use these external interactions see Blueprint at Blueprint SDK — API .

Oracles

Hathor provides built-in support to oracles. An oracle is identified by a TxOutputScript, which is used to ensure that only the oracle can successfully execute a specific public method. To implement a public method for an oracle, use TxOutputScript and SignedData from the Blueprint SDK API. For example, the bet blueprint has the public method set_result:

@public
def set_result(self, ctx: Context, result: SignedData[Result]) -> None:
"""Set final result. This method is called by the oracle."""
self.fail_if_result_is_available()
if not result.checksig(self.oracle_script):
raise InvalidOracleSignature
self.final_result = result.data

By comparing the data of the NC transaction that executed the contract (i.e., called the set_result method) with the oracle_script, the method ensures that final_result can only be set by the oracle. In a nutshell, any user can call the method through a valid NC transaction, but the execution will only be successful if it the transaction was signed by the entity defined as the oracle.

This covers the blueprint side of the oracles support. As for the client part, the SignedData object needs to be properly assembled. This can be done using the headless wallet. To know how to do this, see the Nano contracts with headless tutorial.

Constraints

Some nano contract constraints you need to consider are not unique to Hathor. Rather, they are inherent to the nature of smart contracts in general and are shared across all smart contract platforms. In the next subsections, we’ll look at the most important ones.

Atomic behavior

When modeling your blueprint, remember that the execution of a nano contract has atomic behavior. If the execution fails, nothing happens. If successful, all changes to the contract’s attributes occur, and all actions registered in the calling transaction are executed, thus changing the contract’s balance.

For example, suppose a nano contract ABC has a method abc. During its execution, abc changes the values of several of its attributes. If abc executes to completion without exceptions, it returns nothing (None), and Hathor protocol will execute all the deposits and withdrawals in the NC transaction that called abc. Thus, the state of the ABC contract will be updated.

On the other hand, if an exception occurs during the execution of abc, all attribute changes are discarded, none of the actions are performed, and the contract's state does not change.

Passive behavior

Like conventional smart contracts, nano contracts are passive. They only execute when called through transactions validated on the blockchain. Event-driven logic cannot be implemented in blueprints and must instead be implemented off-chain, in components of integrated systems.

What's next?