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.
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:
- Hathor headless wallet v0.34.0
- Have one wallet on Hathor Network nano-testnet with some amount of HTR. We will use it to interact with the contract.
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
- Create a contract.
- Execute the contract.
- Read the contract.
- 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.
<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:
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 theinitialize
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 blueprintBet
in nano-testnet. In this case is3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595
.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.
- (1)
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
.
- Create a script that specifies Alice as the oracle, replacing
<alice_address>
with an address from your Alice wallet:
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:
{
"success": true,
"oracleData": "76a9141ed32ccd0d28acea3afcf4798e2d4db21401edef88ac"
}
Now that we have the oracle script, we can proceed with creating the contract.
- Submit an NC transaction that calls method
initialize
of blueprintBet
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:
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:
{
"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:
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 frominitialize
.address
: a wallet address that executes the contract.actions
: deposits and withdrawals to be performed in the contract.args
: arguments required bymethod
.
Hands on
Now, let’s execute the bet contract created in the previous task.
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.
- 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:
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.
- 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:
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:
{
"success": true,
"signedResult": "4630440220397d62202a93863ea1918e44e6fc9135bdcb495b4743fbbe66dd99ac2a8b83a902204ba0d6b5b73111f1936bfcd7f11c5a1e7a0aa06fcb13ba9db084cb21e3cb1b3121022e7b7a71a9c252f415ec3e80888cc722b0db083662380ecc43ddb6e489bf4b8a,Real-Madrid3x1Barcelona,str"
}
- For contracts created using the blueprint
Bet
, the oracle will call the methodset_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>
withsignedResult
from previous subtask:
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.
- 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:
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:
{
"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": {}
}
}
- 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:
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:
{
"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,
...
}
]
}
- 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 methodshas_result()
andget_winner_amount()
, replacing<id_of_created_contract>
with your contract ID and adding the required arguments for each method:
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:
{
"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.