Skip to main content

Nano contracts with headless wallet

Introduction

This article is a tutorial to assist developers who already know how to use Hathor headless wallet to get started with nano contracts. By the end of this tutorial, you will have performed all possible actions related to nano contracts.

Background

There are three possible interactions a user can perform with nano contracts: creation, execution, and reading. Additionally, a user can deploy an on-chain blueprint, from which contracts can be created.

tip

Every nano contract provides its blueprint's interface, which specifies how to create, execute, and read it. This interface is wrapped by the API used to integrate a given client with Hathor Network.

In practice, this means that to interact with a contract, the developer should use their client's integration API (whether it is a DApp or any other system) for the generic part of the request, and the specific interface of that contract's blueprint to know which methods to call, how to call them, and which attributes and balances can be read.

Prerequisites

To execute this tutorial, you must meet the following prerequisites:

Overview of the tasks

In this tutorial, we will perform four different tasks. In the first three, we will create, execute, and read a contract. To illustrate, let’s suppose the following scenario. We want to create a betting contract for the result of a football match between Barcelona and Real Madrid. We know that Hathor platform has a built-in blueprint called Bet that is suitable for modeling our use case, and we decide to use it. For simplicity, a single actor — whom we will call Alice — will perform all actions.

Now, suppose the following alternative scenario: we want to create this betting contract with specifications not covered by the blueprint Bet, or any other blueprint already available on Hathor platform. In this case, we decide to develop a new blueprint. Once developed, we need to deploy it on chain to make it available for use. Thus, in the fourth and final task, we will deploy a new blueprint on chain.

Tasks to be executed

  1. Create a contract.
  2. Execute the contract.
  3. Read the contract.
  4. Deploy an on-chain blueprint.

Tasks execution

Now, it's time to get your hands dirty. In each of the next four subsections, we will describe one of the listed tasks.

tip

<Placeholders>: in the code samples of this article, as in all Hathor docs, <placeholders> are always wrapped by angle brackets < >. You shall interpret or replace a <placeholder> with a value according to the context. Whenever replacing a <placeholder> like this one with a value, do not wrap the value with quotes. Quotes, when necessary, will be indicated, wrapping the "<placeholder>" like this one.

To begin, let's suppose you already started your wallet with some HTR as 'Alice' — i.e., "wallet-id": "alice". Also, note that to interact with a contract, you need to know its corresponding blueprint's interface — in this tutorial, the Bet blueprint interface.

Task 1: create a contract

First, we will describe the endpoint for contract creation. Then, we will use it within the flow of a hands-on example of the bet contract scenario described earlier.

POST /wallet/nano-contracts/create

To create a contract, you should submit a nano contract (NC) transaction using the following API request:

API request
curl -X POST \
-H "X-Wallet-Id: <wallet_id>" \
-H 'Content-Type: application/json' \
-d '{
"blueprint_id": "<blueprint_id>",
"address": "<contract_creator>",
"data": {
"actions": [ ... ],
"args": [ ... ]
}
}' \
http://localhost:8000/wallet/nano-contracts/create | jq

Such that:

  • blueprint_id: identifies an existing blueprint on Hathor platform.
  • address: identifies the contract creator.
  • actions: array of deposits and withdrawals that must be made.
  • args: array of arguments to be passed to the initialize method.

actions and args depend on the public method initialize of blueprint_id. Each item in actions is an object with members type, token, and amount, representing either a deposit or a withdrawal. In the following code snippet, we see an example of actions containing one deposit and one withdrawal:

"actions": [
{
"type": "deposit",
"token": "<token_UID_32_bytes_hex>",
"amount": 1000
},
{
"type": "withdrawal",
"token": "00",
"amount": 3500
},
]

Hands on

Alice wants to create a bet contract. To do so, she needs to call initialize from blueprint Bet with the appropriate args and actions. She must submit a nano contract (NC) transaction such that:

  • blueprint_id is the ID of blueprint Bet in nano-testnet. In this case is 3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595.
  • address is an address of your Alice wallet.
  • actions is empty, indicating that Alice does not deposit any tokens at the time of contract creation.
  • args contains three arguments in the following order: (1) oracle_script; (2) token_uid; (3) date_last_bet.
    • (1) oracle_script is a hexadecimal byte stream that specifies the lock script the oracle must resolve to provide the outcome of the bet event. For simplicity, we will define Alice as the oracle of the contract.
    • (2) token_uid is a 32-byte hexadecimal, representing either a token creation transaction ID or 00 for HTR. It specifies the token the bet contract will work with for receiving bets and paying rewards. In this case, it is HTR.
    • (3) date_last_bet is a UNIX-epoch timestamp integer that indicates the deadline for placing bets. Bets typically close shortly before the event begins. To keep this code snippet valid for a long time, the timestamp is set to a distant future date.

Note that to create the contract, we need a script to identify the oracle (oracle_script), which must be sent as the first argument of args.

  1. Create a script that specifies Alice as the oracle, replacing <alice_address> with an address from your Alice wallet:
API request
curl -X GET \
-H "X-Wallet-Id: alice" \
http://localhost:8000/wallet/nano-contracts/oracle-data?oracle=<alice_address> | jq

Using the same address always generates the same lock script. For example, in our case, we use the address WRV28Nwa6hdA6ntRtw264qtEZMX7p5EJCq, and the output will always be 76a9141ed32ccd0d28acea3afcf4798e2d4db21401edef88ac, which is the hexadecimal representation of the lock script that identifies Alice as the oracle:

API response
{
"success": true,
"oracleData": "76a9141ed32ccd0d28acea3afcf4798e2d4db21401edef88ac"
}

Now that we have the oracle script, we can proceed with creating the contract.

  1. Submit an NC transaction that calls method initialize of blueprint Bet to create a new contract, replacing <contract_creator> with an address of your Alice wallet and <oracle_script> with the oracle script you generated in the previous step:
API request
curl -X POST \
-H "X-Wallet-Id: alice" \
-H 'Content-Type: application/json' \
-d '{
"blueprint_id": "3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595",
"address": "<contract_creator>",
"data": {
"actions": [],
"args": [
"<oracle_script>",
"00",
1830708900
]
}
}' \
http://localhost:8000/wallet/nano-contracts/create | jq

A contract is identified by its ID, which is the hash of the transaction that created it. For example:

API response
{
"success": true,
"inputs": [],
"outputs": [],

...

"tokens": [],
"hash": "000063f99b133c7630bc9d0117919f5b8726155412ad063dbbd618bdc7f85d7a",
"_dataToSignCache": {
...
},
"id": "3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595",
"method": "initialize",
"args": [
...
],
...
}

Task 2: execute the contract

Analogous to the previous section, we will first describe the contract execution endpoint, then proceed with the flow of our hands-on example involving Alice and the bet contract.

POST /wallet/nano-contracts/execute

To execute a contract, you should submit a nano contract (NC) transaction using the following API request:

API request
curl -X POST \
-H "X-Wallet-Id: <wallet_id>" \
-H 'Content-Type: application/json' \
-d '{
"nc_id": "<hash_of_tx_that_created_contract>",
"method": "<public_method_of_contract>",
"address": "<address_of_executor>",
"data": {
"actions": [ ... ],
"args": [ ... ]
}
}' \
http://localhost:8000/wallet/nano-contracts/execute | jq

Such that:

  • nc_id: identifies a contract registered (created) on the ledger (blockchain).
  • method: name of the public method of the contract to be called, different from initialize.
  • address: a wallet address that executes the contract.
  • actions: deposits and withdrawals to be performed in the contract.
  • args: arguments required by method.

Hands on

Now, let’s execute the bet contract created in the previous task.

tip

Representing the quantity of tokens: in Hathor headless wallet API requests and responses, the standard to represent any amount of fungible tokens — i.e., the value property of inputs and outputs objects — is using an integer number whose two last digits are the cents — e.g., 10 HTR becomes '1000', 10.50 HTR becomes '1050', and so forth.

In turn, the standard to represent any number of non-fungible tokens (NFTs) is using an integer that indeed stands for an integer number — e.g., '10' of some NFT stands for ten units of that token.

  1. Alice assumes the role of a bettor. She will bet 10 HTR that the result of the Real Madrid vs. Barcelona match will be 2 x 2. To place this bet, submit the following NC transaction calling the method bet, replacing <id_of_created_contract> with the ID of the contract you created, and <address_identifies_bettor> with an address of your Alice wallet:
API request
curl -X POST \
-H "X-Wallet-Id: alice" \
-H 'Content-Type: application/json' \
-d '{
"nc_id": "<id_of_created_contract>",
"method": "bet",
"address": "<address_identifies_bettor>",
"data": {
"actions": [
{
"type": "deposit",
"token": "00",
"amount": 1000
}
],
"args": [
"<address_identifies_bettor>",
"Real-Madrid2x2Barcelona"
]
}
}' \
http://localhost:8000/wallet/nano-contracts/execute | jq

Receiving "success": true in your API response means that the transaction has been submitted to the network, validated by a full node, and added to the mempool. However, this does not mean that the contract execution was successful. Contract execution will only occur after the next block is mined and the transaction is added to the blockchain.

Now, suppose the match has concluded with the result Real Madrid 3 x 1 Barcelona. Alice takes the role of the oracle and should submit this result to the contract. The result should be expressed in one of the following data types: string, number, or boolean.

  1. To submit the result, the contract must include a method exclusively for the oracle's use. To verify that the method is being called by the oracle, the result must be submitted with a signature. Use Alice's wallet to generate the signed result, replacing <your_oracle_script> with the script you obtained as a result in step 1 of task 1:
API request
curl -X GET \
-H "X-Wallet-Id: alice" \
"http://localhost:8000/wallet/nano-contracts/oracle-signed-result?oracle_data=<your_oracle_script>&type=str&result=Real-Madrid3x1Barcelona" | jq

For example, with our script 76a9141ed32ccd0d28acea3afcf4798e2d4db21401edef88ac, we obtained the following signed result:

API response
{
"success": true,
"signedResult": "4630440220397d62202a93863ea1918e44e6fc9135bdcb495b4743fbbe66dd99ac2a8b83a902204ba0d6b5b73111f1936bfcd7f11c5a1e7a0aa06fcb13ba9db084cb21e3cb1b3121022e7b7a71a9c252f415ec3e80888cc722b0db083662380ecc43ddb6e489bf4b8a,Real-Madrid3x1Barcelona,str"
}
  1. For contracts created using the blueprint Bet, the oracle will call the method set_result to submit the match result. To do this, submit the following NC transaction,replacing <id_of_created_contract> with your contract ID, <some_address_your_alice_wallet> with some address of your Alice wallet, and <result> with signedResult from previous subtask:
API request
curl -X POST \
-H "X-Wallet-Id: alice" \
-H 'Content-Type: application/json' \
-d '{
"nc_id": "<id_of_created_contract>",
"method": "set_result",
"address": "<some_address_your_alice_wallet>",
"data": {
"actions": [ ],
"args": [
<result>
]
}
}' \
http://localhost:8000/wallet/nano-contracts/execute | jq

Task 3: read the contract

There are three types of readings that can be made in a contract. One can read the (1) state, (2) history, or (3) query the contract.

  1. Reading the contract's state allows you to view its attribute values and balance. Read the state of your bet contract, replacing <id_of_created_contract> with your contract ID:
API request
curl -X GET \
-H "X-Wallet-Id: alice" \
"http://localhost:8000/wallet/nano-contracts/state?id=<id_of_created_contract>&fields[]=token_uid&fields[]=date_last_bet&fields[]=total&fields[]=final_result&balances[]=00" | jq

You send the API request to the headless wallet. It forwards the request to the full node it is connected to (i.e., its server), which then responds with the attributes specified in the query parameters. For example:

API response
{
"success": true,
"state": {
"success": true,
"nc_id": "000063f99b133c7630bc9d0117919f5b8726155412ad063dbbd618bdc7f85d7a",
"blueprint_id": "3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595",
"blueprint_name": "Bet",
"fields": {
"token_uid": {
"value": "00"
},
"date_last_bet": {
"value": 1830708900
},
"total": {
"value": 1000
},
"final_result": {
"value": "Real-Madrid3x1Barcelona"
}
},
"balances": {
"00": {
"value": "1000"
}
},
"calls": {}
}
}
  1. The contract history consists of a list starting with the transaction that created it, followed by all transactions that executed it in chronological order. Read the history of your bet contract, replacing <id_of_created_contract> with your contract ID:
API request
curl -X GET \
-H "X-Wallet-Id: alice" \
http://localhost:8000/wallet/nano-contracts/history?id=<id_of_created_contract> | jq

The contract's history includes all NC transactions that called any public method of the contract. It also includes transactions where contract execution failed, marked as 'voided'. In our example, we can see the transactions submitted to set the result of the match, place a bet, and create the contract, respectively:

API response
{
"success": true,
"history": [
{
...
"nc_id": "000063f99b133c7630bc9d0117919f5b8726155412ad063dbbd618bdc7f85d7a",
"nc_blueprint_id": "3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595",
"nc_method": "set_result",
...
"tx_id": "00002a47589a1bb388f72b2b007a429a5a626a3047a37fd4eaa2b7da006b6c21",
"is_voided": false,
...
},
{
...
"nc_id": "000063f99b133c7630bc9d0117919f5b8726155412ad063dbbd618bdc7f85d7a",
"nc_blueprint_id": "3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595",
"nc_method": "bet",
...
"nc_context": {
"actions": [
{
"type": "deposit",
"token_uid": "00",
"amount": 1000
}
],
...
},
"tx_id": "0000000030a0463488a238a5e9803ce144c29e95a712af54d38ee2cc9c41a8b4",
"is_voided": false,
...
},
{
...
"nc_id": "000063f99b133c7630bc9d0117919f5b8726155412ad063dbbd618bdc7f85d7a",
"nc_blueprint_id": "3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595",
"nc_method": "initialize",
...
"tx_id": "000063f99b133c7630bc9d0117919f5b8726155412ad063dbbd618bdc7f85d7a",
"is_voided": false,
...
}
]
}
  1. Querying the contract involves retrieving data that is not directly stored in its state but can be calculated ad hoc by a full node. It uses the same endpoint as reading the state but with the query parameter calls[], an array of view methods to be called. Query your bet contract by calling the view methods has_result() and get_winner_amount(), replacing <id_of_created_contract> with your contract ID and adding the required arguments for each method:
API request
curl -X GET \
-H "X-Wallet-Id: alice" \
"http://localhost:8000/wallet/nano-contracts/state?id=<id_of_created_contract>&calls[]=has_result()&calls[]=get_winner_amount(%22a%27WRV28Nwa6hdA6ntRtw264qtEZMX7p5EJCq%27%22)" | jq

Note that the view method has_result() does not require any arguments. In turn, get_winner_amount() requires one argument, specifically, the address to be consulted. In the example, we use the same address WRV28Nwa6hdA6ntRtw264qtEZMX7p5EJCq that has been used since the beginning of the tutorial. Note that this endpoint expects addresses prepended with a'. For example, a'WRV28Nwa6hdA6ntRtw264qtEZMX7p5EJCq. Additionally, we use percent-encoding for double and single quotes in the URL for HTTP data transmission.

The full node serving your headless wallet will invoke the view methods and return the computed values. In this case, it will indicate whether the oracle has already submitted the match result and how much winnings are available to the bettor address. For example:

API response
{
"success": true,
"state": {
"success": true,
"nc_id": "000063f99b133c7630bc9d0117919f5b8726155412ad063dbbd618bdc7f85d7a",
"blueprint_id": "3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595",
"blueprint_name": "Bet",
"fields": {},
"balances": {},
"calls": {
"has_result()": {
"value": true
},
"get_winner_amount(\"a'WRV28Nwa6hdA6ntRtw264qtEZMX7p5EJCq'\")": {
"value": 0
}
}
}
}

Task 4: deploy an on-chain blueprint

The headless wallet does not yet support the deployment of on-chain blueprints. For now, if you are developing a blueprint and need to test it, see How to develop blueprints.

Tasks completed

At this point, you have performed all possible nano contract actions using the headless wallet. These four tasks provide examples of contract creation, contract execution by a typical user, execution by an oracle, all types of contract readings, and on-chain blueprint deployment.

Key takeaways

However, note that each blueprint — and therefore all instantiated contracts — has a unique interface. This interface specifies the set of public and view methods implemented specifically by that blueprint. For example, in the Bet blueprint, calling the initialize method to create a new contract requires exactly zero deposit actions. In another blueprint, initialize might require two deposit actions with two different tokens. Keep in mind that each public method in a blueprint specifies not only the parameters it should receive but also the deposit and withdrawal actions that must accompany it. For instance, consider two hypothetical blueprints: Future and CallOption. Both may have a public method with the same name, payment. However, payment in Future requires parameters, deposits, and withdrawals that are entirely different from those required by payment in CallOption.

Additionally, understanding the contract's behavior is crucial. This includes identifying its actors, understanding its life cycle, and knowing how to use each method provided by the interface.

In summary, beyond knowing how to perform actions using the headless wallet, you must also know both the interface and the behavior of the blueprint you are working with.

What's next?

Hathor headless wallet HTTP API reference: to consult while implementing nano contracts into your use case.