Skip to main content

Blueprint development

Introduction

This article is the reference material to consult while developing a blueprint. It requires the prior reading of Nano contracts: how it works.

Nano contracts flow

This section presents the nano contracts flow in the level of detail necessary for blueprint developers. The nano contracts flow can be divided into three phases:

  • Validation
  • Method execution
  • Resolution

Validation

Every creation and execution of a nano contract results from a user submitting an NC transaction to Hathor Network. In other words, everything starts when a user submits an NC transaction to a full node. An NC transaction is a subtype of transaction that has three specific fields not present in other transactions:

  • nc_id
  • nc_method
  • nc_args

Hathor protocol then begins the validation phase of the transaction. First, the common validations applicable to all transactions are performed. If the transaction passes these validations, the specific validations for NC transactions are then conducted:

  • Validation of nc_id
  • Validation of nc_method
  • Validation of withdrawals

Validation of nc_id

Hathor protocol verifies whether nc_id identifies an existing blueprint on Hathor platform or a nano contract registered on chain. If it does not, the transaction is considered invalid and discarded. If nc_id identifies a blueprint, Hathor protocol understands that the user wants to create a new contract. Conversely, if nc_id identifies a nano contract registered on chain, Hathor protocol understands that the user wants to execute an existing contract.

Validation of nc_method

Hathor protocol verifies if nc_method identifies a valid public method to be executed. In the case where nc_id identifies a blueprint — that is, contract creation — nc_method must be initialize. For example, let’s take the case of the swap blueprint from our tutorial. To create a swap contract, a user must submit an NC transaction such that:

  • nc_id: identifier of the blueprint on Hathor platform.
  • nc_method: 'initialize'

In the case where nc_id identifies a nano contract (registered on chain), nc_method must be the name of an existing public method in the contract, different from initialize. For example, to execute the swap method in a contract created from the swap blueprint, a user must submit an NC transaction such that:

  • nc_id: id of the transaction that registered the contract creation.
  • nc_method: 'swap'

If it does not fit into either of these cases, the transaction is considered invalid and discarded. This will occur in the following cases:

  • nc_id points to a blueprint and nc_method is not initialize.
  • nc_id points to a nano contract and nc_method is initialize.
  • nc_id points to a nano contract and nc_method is not an existing public method in the contract other than initialize.

Validation of withdrawals

To validate withdrawals, Hathor protocol first translates the set of inputs and outputs into a set of NCAction (actions). An NCAction object is a tuple that contains:

  • type of the action, which can be DEPOSIT or WITHDRAWAL.
  • token_uid that identifies the token to which the deposit or withdrawal action refers.
  • amount that describes the quantity (amount) of tokens to be transferred to or from the contract's balance.

Hathor protocol then verifies if the contract has sufficient funds to perform all requested withdrawal actions. If the contract does not have enough funds to perform all withdrawals, the transaction is considered invalid and discarded. For example, suppose a nano contract with the following multi-token balance:

  • Token A: 100
  • Token B: 10
  • Token C: 50
  • Token D: 20

In this case, the following sets of actions will be considered valid (individually):

  • Alice wants to: deposit 10 tokens A; withdraw 10 tokens B; withdraw 10 tokens C.
  • Bob wants to: withdraw 10 tokens A; withdraw 10 tokens B; deposit 20 tokens E.
  • Charlie wants to: withdraw 20 tokens D; deposit 10 tokens E; deposit 10 tokens F.

And the following sets of actions will be considered invalid (individually):

  • Alice wants to: withdraw 120 tokens A; deposit 30 tokens F.
  • Bob wants to: withdraw 10 tokens A; withdraw 15 tokens B.
  • Charlie wants to: withdraw 10 tokens G.

Another case in which Hathor protocol will consider the request invalid is when a contract creation transaction contains a withdrawal request. Obviously, if a contract is being created now, its balance for any token will always be zero, and thus no withdrawal makes sense. Hathor protocol considers the transaction invalid and discards it. The following diagram shows a case in which this occurs:

Example of invalid NC transaction

(Invalid NC transaction: withdrawal with initialize)(\text{Invalid NC transaction: withdrawal with initialize})

Alice calls the initialize method to create a contract from a blueprint but requests a withdrawal of 10 tokens B, which does not make sense since a newly created contract will always have an initial balance of zero for any token.

Note that Hathor protocol does not invalidate an NC transaction due to deposits. For Hathor protocol, if the user has sufficient funds in their wallet to make the deposit, then the deposit request is valid. It will be up to the contract, during its method execution, to decide whether or not to authorize this deposit request.

Awaiting in the mempool

Once the NC transaction passes the common checks, and the specific validations for nc_id, nc_method, and withdrawals, it is considered valid and sent to the mempool to await the arrival of the next block. When the next block arrives, all transactions waiting in the mempool will be added to the blockchain. At this point, the public method for creating or executing the nano contract will be called.

Hathor protocol calls the method defined in the NC transaction, passing as arguments a Context object followed by all the arguments contained in nc_args, in the same order they appear. A Context object provides:

  • tx: the transaction ID that called the method.
  • address: the wallet address that called the method.
  • timestamp: the timestamp of the first block confirming the transaction.
  • actions: a dictionary where the keys are token_uid and the values are NCAction objects.

Note that the set of deposits and withdrawals the user wants to perform with the transaction is found in actions.

Method execution

The method call initialize denotes contract creation, while the call of any other public method denotes contract execution.

Contract creation

Every blueprint must have the initialize method. The initialize method typically performs the following activities:

  1. Verify that the set of received arguments is valid.
  2. Authorize or deny the creation of a new contract.
  3. Define the initial state of the attributes for this new contract.

For example, suppose Alice wants to create a new nano contract from a certain blueprint called "ABC":

  • She submits an NC transaction to Hathor Network that correctly describes the creation of this contract. This transaction has three deposits: 10 tokens A, 20 tokens B, and 30 tokens C, and no withdrawals.
  • Hathor protocol considered the transaction valid, and upon the arrival of the next block, called the initialize method of the blueprint for execution.
  • The initialize method confirmed that the received arguments are valid for the creation of a new contract.
  • initialize will use the arguments passed by Alice in nc_args and the Context to initialize the attributes of the new contract.
  • initialize executes to the end and returns nothing (None). This indicates to Hathor protocol that the contract creation was successful.

On the other hand, suppose Bob wants to create another nano contract using this same blueprint "ABC":

  • However, he submits an NC transaction to Hathor Network that does not correctly describe the creation of this contract. This transaction has no deposits (which is not what initialize expects) and no withdrawals.
  • Hathor protocol considered the transaction valid, and upon the arrival of the next block, called the initialize method of the blueprint for execution.
  • The initialize method was expecting exactly three deposits for tokens A, B, and C respectively (as Alice did). Since it did not receive exactly these three deposits, initialize raises an exception. This indicates to Hathor protocol that the contract creation failed.

Contract execution

Executing a contract denotes that the public method (other than initialize) typically performs the following activities:

  1. Verify that the set of received arguments is valid.
  2. Authorize or deny the execution of all deposits and withdrawals that the user wants to make.
  3. Update the contract's attributes.

For example, suppose Alice wants to execute a certain nano contract that was instantiated from the swap blueprint:

  • She submits an NC transaction to Hathor Network that correctly describes the call to the public method swap of this contract. This transaction has a deposit of 10 tokens A and a withdrawal of 20 tokens B.
  • Hathor protocol considers the transaction valid, and upon the arrival of the next block, calls the swap method for execution.
  • The swap method confirms that the received arguments are valid.
  • swap make the checks to verify if the request should be authorized.
  • swap updates the contract's attributes.
  • swap executes to the end and returns nothing (None). This indicates to Hathor protocol that the contract execution was successful, which means the set of all deposits and withdrawals of the NC transaction was authorized.

On the other hand, suppose Bob wants to execute the same contract, calling the same swap method:

  • He submits an NC transaction to Hathor Network that does not correctly describe the call to the public method swap. This transaction has a deposit of 100 tokens C and no withdrawal.
  • Hathor protocol considers the transaction valid, and upon the arrival of the next block, calls the swap method for execution.
  • The swap method was expecting exactly two actions, one for deposit and one for withdrawal. Since this did not occur, swap raises an exception. This indicates to the Hathor protocol that the contract execution failed, and thus, the requested deposit was not authorized.

Resolution

Hathor protocol receives the method's return and completes the processing of the NC transaction. There are two possible outcomes for the creation/execution of a contract:

  • Success
  • Fail

Success

If the public method runs to completion without raising an exception, Hathor protocol understands that the creation/execution was successful and will carry out the following activities:

  • In the case of contract creation, add it to the database (i.e., the ledger).
  • In the case of contract execution, update the attributes as modified by the method.
  • Perform all deposits and withdrawals contained in the NC transaction and update the contract's balance.
  • Add the NC transaction to the blockchain marked as "success".

Fail

If the execution of the public method raises an exception, Hathor protocol understands that the creation/execution has failed and will carry out the following activities:

  • In the case of contract creation, discard the created instance.
  • In the case of contract execution, discard any modifications made to attributes.
  • Do not perform any of the deposits and withdrawals contained in the NC transaction.
  • Add the NC transaction to the blockchain marked as "fail" and with its "inputs/outputs" voided.

Guidelines

This section presents guidelines for blueprint development.

Programming languages

Hathor core provides an SDK for blueprint development in Python 3. It is possible that in the future the blueprint SDK will be expanded to other languages. For now, you will need to develop your blueprint using Python 3.

Hathor protocol validations

As explained in the previous section (Nano contracts flow), Hathor protocol performs a series of validations before invoking a public method of a blueprint. If an NC 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

Attributes

Blueprints do not support class attributes. In object-oriented programming, such as in Python, which is used for blueprint development in Hathor's SDK, 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 permitted in blueprint development. 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

This subsection outlines key considerations for designing the methods of your blueprint. Now, for an overall view about methods, see the Methods section in Nano contracts: how it works.

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. 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 protocol and is only updated after a public method completes successfully without throwing an exception, thereby authorizing all proposed deposits and withdrawals. The next subsection will detail this balance update process.

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.

Changes to the contract balance

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 nc_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 subsection, 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.).

Current capabilities

Blueprints are composable. Through Python's multiple inheritance, it is possible to develop blueprints that reuse and extend other blueprints, making them modular and flexible. For example, in the current catalog of Hathor platform, there is the bet blueprint. You can develop a blueprint called SportsBet that will be a subclass of the Bet class:

class SportsBet(Bet):
"""Sports bet blueprint that specifies useful stuff for sport events."""
...

Furthermore, a blueprint can inherit from multiple other blueprints simultaneously. For example, in the current catalog of the Hathor platform, there is the liquidity pool blueprint. You can develop a blueprint called LiquiditySports that is a subclass of both the Bet and LiquidityPool classes:

class LiquiditySports(Bet, LiquidityPool):
"""Liquidity pool for swapping placed bets."""
...

Nano contracts support oracles. An oracle is an individual or organization that provides off-chain data that a contract needs to operate. You can define none, one, or multiple oracles to address different data demands in your blueprint. For example, in a sports bet contract, an oracle is required to inform the result of a match.

Each oracle is identified by a TxOutputScript. You will use this to ensure that only those defined as oracles can successfully execute a certain public method. 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 will only allow the final_result to 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.

Future capabilities

In the future, nano contracts will be interoperable. This means that the execution of one nano contract will be able to call the execution of another nano contract. For example, suppose an NC transaction calls the execution of the abc method of the ABC contract. During its execution, the abc method of the ABC contract could call the execution of the cde method of the CDE contract.

This results in the possibility of call chains involving multiple nano contracts. For example, when called by ABC, the cde method of the CDE contract could call the execution of the efg method of the EFG contract, and so on.

Key specifications

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.

Like all 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. Note that this is not unique to Hathor; it is common across all smart contract platforms.

Parameters

When designing the business logic of your methods, you will have the following data available to manipulate:

  1. The state of the contract itself.
  2. The arguments passed by the user in the NC transaction in nc_args.
  3. Information about the calling NC transaction and the current state of the blockchain via Context.

Take, for example, the bet blueprint available on the blueprints catalog of Hathor platform. In the snippet below, we see the signature of the bet method of this blueprint:

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

Following Python best practices, self denotes the object itself, providing access to the contract's attributes' state. The second parameter must always be a Context object, followed by parameters for all arguments in the same order they are passed in the NC transaction in nc_args.

Note that self provides access to the contract's attributes and methods, but not to its multi-token balance. As mentioned earlier, the contract cannot alter its balance (it can only authorize Hathor protocol to make such changes), and to read its balance, it needs to use methods in Context, as explained in the next subsection.

Reading the contract balance

As previously mentioned, the multi-token balance of a contract is not one of its attributes. As a result, to read its balance, a contract needs to use the following methods of the Context object:

  • get_balance(token_id: TokenUid) -> Amount
    • Reads the balance of the contract for a given token.
  • get_htr_balance() -> Amount
    • Reads the balance of the contract for HTR.

In the snippet below, we see examples of using these methods:

@public
def foo(self, ctx: Context, token_id: TokenUid) -> None:
...
foo_balance = ctx.get_balance(token_id)

@view
def bar(self) -> None:
...
bar_balance = ctx.get_htr_balance()

Note that the contract needs to know which token it wants to check the balance of. That is, it cannot discover at runtime which tokens it has in its multi-token balance. Therefore, it is necessary to have attributes that store which tokens the contract operates with (or are currently operating with).

Return value

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

Data types

This section presents the types of data you will need or can use when developing your blueprint.

Context

Every public method must have a Context object as its second parameter. Context provides the method with all the information about the NC transaction and the state of the blockchain. Context has the following attributes:

  • tx: BaseTransaction object that contains the basic data of the NC transaction that called the method.
  • address: Address object that denotes the address that called the method.
  • timestamp: integer that denotes the timestamp of the first block confirming the transaction.
  • actions: a dictionary where the keys are token_uid and the values are NCAction objects.

Context has the following methods:

  • get_balance(token_id: TokenUid) -> Amount
    • Reads the balance of the contract for a given token.
  • get_htr_balance() -> Amount
    • Reads the balance of the contract for HTR.

NCAction

NCAction describes an action (deposit or withdrawal) that a user wants to make to/from the contract, translated from the consolidated inputs and outputs of the NC transaction. An NCAction object is a NamedTuple such that:

  • type: NCActionType object that denotes the type of action (deposit or withdrawal).
  • token_uid: bytes object that identifies a token on Hathor blockchain.
  • amount: amount of tokens involved in the action.

Thus, the fields of an NCAction define an action to "make an NCActionType of amount tokens token_uid." For example, "make a deposit of 10 tokens A" corresponds to NCAction:

action_a:
type: DEPOSIT
token_uid: <token_A_UID>
amount: 10

Here is a sample code for this action:

action_a = (NCActionType.DEPOSIT, b'<token_A_UID>', 10)

And "make a withdrawal of 10 tokens B" corresponds to NCAction:

action_b:
type: WITHDRAWAL
token_uid: <token_B_UID>
amount: 10

Here is a sample code for this action:

action_b = (NCActionType.WITHDRAWAL, b'<token_B_UID>', 10)

NCActionType

NCActionType is an enumerator that describes the types of interactions a transaction can have with a contract. There are two possible types of actions:

  • DEPOSIT for deposit.
  • WITHDRAWAL for withdrawal.

Core data types

This section presents the core data types in Hathor that are relevant for blueprint development:

  • VertexId: an alias for bytes that denotes a transaction on Hathor blockchain. A transaction has 32 bytes.
  • TokenUid: an alias for VertexId that denotes a token registered on Hathor blockchain. A token is identified by the id of the transaction that created it.
  • Timestamp: an alias for integer. Integer timestamps follow the Unix epoch timestamp standard, meaning they denote the time in seconds elapsed since January 1st, 1970 00:00:00 UTC. For example, the timestamp for July 11, 2024, at noon UTC is 1720699200.
  • Amount: an alias for integer that denotes an amount of tokens. The last two digits are decimals. For example, an Amount object with the value 1025 denotes 10.25 tokens.
  • AddressB58: an alias for string. It denotes a wallet address represented as an alphanumeric string.
  • Address: an alias for bytes. It denotes a wallet address represented as 20 bytes.
  • TxOutputScript: an alias for bytes that denotes the output script of a transaction.

What's next?