Skip to main content

Read-only mode with headless wallet

Introduction

This article is a tutorial to assist developers who already use Hathor headless wallet to get started with its wallet read-only mode. By the end of this tutorial, you will have performed your first transaction in Hathor headless wallet using the read-only mode.

Glossary

This term is used throughout this article:

  • Integrated system is any system already integrated with Hathor system.

Background

Read-only wallet is a mode of using a wallet application. In the read-only mode the wallet application does not store the wallet's private key and, consequently, cannot sign transactions. To know more about read-only wallets in Hathor, see About read-only wallets.

However, it is still possible to create unsigned transactions and work with another module of a wallet composite system to complete a transaction. This is precisely the task we will perform in this tutorial. For a better understanding of how such wallet composite system works with read-only wallets, see Functioning of read-only wallets for integrated systems.

Prerequisites

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

  • Hathor headless wallet \ge v0.19.2
  • Have two different wallets on Hathor Network testnet.
  • One of these wallets must have at least a small amount of HTR. We will use this amount to perform a transfer between both wallets.

Overview of the task

Let's suppose two users, Alice and Bob. Alice wants to transfer X amount of HTR to Bob. Let the wallet with a small amount of HTR be Alice's wallet, whereas the other be Bob's. Thus, Alice's wallet will be the remitter, whereas Bob's wallet will be the transfer recipient. Bob's role is secondary. He will just provide an address to Alice to allow her to perform the transfer.

At first glance, this transfer looks like the most ordinary blockchain transaction we could thought. However, Alice's integration architecture makes it a little more complicated. This is because Alice does not use her wallet on Hathor headless wallet application in default mode. Instead, she uses it in read-only mode and cannot sign transactions directly from Hathor headless wallet.

Thus, she uses Hathor headless wallet along with a KMS (key management service), which runs in a more protected environment. With such an arrangement, she does many wallet management actions using the read-only wallet but relies on the KMS to store the private key and sign the transactions. She does it for security reasons.

Sequence of steps

We will represent all steps Alice needs to do in order to transfer X amount of HTR to Bob using her read-only wallet along with his KMS. To accomplish the task, you will follow these steps:

  1. Generate Alice's extended public key.
  2. Start Alice's wallet in read-only mode.
  3. Create a transaction proposal.
  4. Obtain inputs information.
  5. Generate inputs signatures.
  6. Generate signed inputs data.
  7. Append signed inputs data into transaction proposal.
  8. Pus signed transaction proposal.

Task execution

Now, it's time to get your hands dirty. In this section, we will describe the steps in detail.

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 Alice still needs to start her wallet in Hathor headless wallet in read-only mode.

Step 1: generate Alice's extended public key

To use a wallet in read-only mode, you must start it using the extended public key instead of the seed phrase. If you already have Alice's extended public key, you can proceed to the next step. Otherwise, Hathor headless wallet has a script that receives a seed phrase and returns its derived extended public key.

If you run Hathor headless wallet from source code, use the following substeps to generate the extended public key:

  1. Open the command line from the directory where you installed Hathor headless wallet.
  2. Run the get_xpub_from_seed.js script, replacing the <alice_24_words_seed_phrase> placeholder with Alice's wallet seed phrase:
make xpub_from_seed seed="<alice_24_words_seed_phrase>"

Regardless the substeps you followed, you should receive <alice_xpub_key> as return.

Step 2: start Alice's wallet in read-only mode

Whereas to use a wallet in default mode you need its seed phrase, to use a wallet in read-only mode you need its extended public key. Now, start Alice's wallet in read-only mode using <alice_xpub_key>:

API request
curl -X POST \
-H 'Content-Type: application/json' \
-d '{
"xpubkey": "<alice_xpub_key>",
"wallet-id": "alice"
}' \
http://localhost:8000/start | jq

As already discussed, you can perform many wallet management actions in read-only mode, such as consulting balances, checking transaction history, and even creating transaction proposals. The paramount exception is signing transactions.

Step 3: create a transaction proposal

Let's create a transaction proposal where Alice transfers X HTR to Bob's address <bob_address>. We use <Xxx> rather than <X> to represent the HTR amount to remind you that the last two digits of value must be the cents, without a comma or point separation.

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.

API request
curl -X POST \
-H 'X-Wallet-Id: alice' \
-H 'Content-Type: application/json' \
-d '{
"outputs": [
{
"address": "<bob_address>",
"value": <Xxx>
}
]
}' \
http://localhost:8000/wallet/tx-proposal | jq

The API response provides txHex and dataToSignHash. txHex is an unsigned transaction proposal. dataToSignHash is the data that will be used to generate the input signature:

API response
{
"success": true,
"txHex": "<unsigned_tx_proposal>",
"dataToSignHash": "<data_to_generate_input_signature>"
}

Step 4: obtain inputs information

In Hathor, signing a transaction means authorizing the spending — as inputs — of a set of UTXOs located in some wallet addresses. Since Alice stores its private key only in her KMS, we need to request the KMS the authorization to spend each UTXO. We make this request by sending it the dataToSignHash along with the address path where each UTXO is located.

Let's obtain our inputs information that states the address of all UTXOs we are aiming to spend:

API request
curl -X GET \
-H 'X-Wallet-Id: alice' \
http://localhost:8000/wallet/tx-proposal/get-wallet-inputs?txHex=<unsigned_tx_proposal> | jq

The API response provides Alice with an array of inputs. The addressPath property of each input object of the inputs array provides the data we need to send to the KMS in order to it localize and authorize the spending of each UTXO:

API response
{
"success": true,
"inputs": [
{
"inputIndex": 0,
"addressIndex": <address_index_x>,
"addressPath": "m/44'/280'/0'/0/<address_index_x>"
},
{
"inputIndex": 1,
"addressIndex": <address_index_y>,
"addressPath": "m/44'/280'/0'/0/<address_index_y>"
},
...
{
"inputIndex": i,
"addressIndex": <address_index_j>,
"addressPath": "m/44'/280'/0'/0/<address_index_j>"
},
...
{
"inputIndex": n,
"addressIndex": <address_index_z>,
"addressPath": "m/44'/280'/0'/0/<address_index_z>"
},
]
}

The "<addressPath_input_index_i>" placeholder refers to the address path of value "m/44'/280'/0'/0/<address_index_j>", associated to the input of index i, not the index i itself. Furthermore, the index j has no relation with index i. Whereas i indexes each of the n inputs of the transaction proposal we created, j indexes some random address of Alice's wallet, where one UTXO is located.

Step 5: generate signatures

Now Alice needs to generate a set of signatures. Each signature authorizes spending one of the UTXOs assigned to the transaction. This is precisely what cannot be done using the read-only wallet mode. As already discussed, to do this Alice will use her KMS.

Alice provides the dataToSignHash and the addressPath of each UTXO. The KMS then returns a set of signatures. Each of these signatures is associated with one input of the transaction.

To do this step, you may use your own external signing method in the role of Alice's KMS. For example, in this tutorial, we use the following script to generate the signature of each input:

generate_input_signature.js
const hathorLib = require('@hathor/wallet-lib');

const seed = '<alice_24_words_seed_phrase>';
const addressPath = "<addressPath_of_input_index_i>";
const dataToSignHash = '<dataToSignHash>';

const xprivRoot = hathorLib.walletUtils.getXPrivKeyFromSeed(seed, { networkName: 'mainnet' });
const xpriv = xprivRoot.deriveNonCompliantChild(addressPath);
const signature = hathorLib.transactionUtils.getSignature(Buffer.from(dataToSignHash, 'hex'), xpriv.privateKey);

console.log(signature.toString('hex'));

We added this tutorial within the Hathor headless wallet source code to use Hathor wallet lib.

tip

If for tests purposes you want to use the previous script, you may add the generate_input_signature.js within scripts of Hathor headless wallet source code and append the key/pair "generate_input_signature": "babel-node scripts/generate_input_signature.js" at the end of the scripts object of the package.json file.

danger

Note that we provide such script here for test purposes only. Using it in the same environment as the read-only wallet defeats the purpose of using this wallet mode.

Regardless of the alternative to generate the input signature, you should receive the signature associated with the input index i as a return. You must repeat the process n times, one for each input index i, from 0 to n. At the end of this iteration, you will have n signatures, each associated with one of the n inputs of the transaction.

Step 6: generate signed inputs data

With all n input signatures in hands, Alice will use them to generate the respectively signed inputs data:

API request
curl -X POST \
-H 'X-Wallet-Id: alice' \
-H 'Content-Type: application/json' \
-d '{
"index": "<address_index_j>",
"signature": "<signature_index_i>"
}' \
http://localhost:8000/wallet/tx-proposal/input-data | jq

The API response provides inputData, the signed input index i data:

API response
{
"success": true,
"inputData": "<signed_input_data_index_i>"
}

You must repeat the process n times, one for each input index i, from 0 to n. At the end of this iteration, you will have n input data, each associated to one of the n inputs of the transaction.

Step 7: append signed input data into transaction proposal

With all signed input data, Alice will append the array of signed input data to the transaction proposal:

API request
curl -X POST \
-H 'X-Wallet-Id: alice' \
-H 'Content-Type: application/json' \
-d '{
"txHex": "<tx_hex>",
"signatures": [
{
"index": 0,
"data": "<signed_input_data_index_0>"
},
{
"index": 1,
"data": "<signed_input_data_index_1>"
},
...
{
"index": i,
"data": "<signed_input_data_index_i>"
},
...
{
"index": n,
"data": "<signed_input_data_index_n>"
}
]
}' \
http://localhost:8000/wallet/tx-proposal/add-signatures | jq

The API response provides a signed transaction proposal, ready for submission to Hathor Network:

API response
{
"success": true,
"txHex": "<signed_tx_proposal>"
}

Step 8: push signed transaction proposal

Finally, Alice will push the signed transaction proposal to Hathor Network:

API request
curl -X POST \
-H 'X-Wallet-Id: alice' \
-H 'Content-Type: application/json' \
-d '{
"txHex": "<signed_tx_proposal>"
}' \
http://localhost:8000/push-tx | jq

If the transaction proposal is complete, signed and in a valid state, it shall be validated and recorded into the blockchain:

API response
{
"success":true,
"tx": {
"hash": "<tx_hash>",
...
"inputs": [
{
...
"index": 1,
"data": ...
},
...
],
...
"tokens": []
}
}

Task completed

At this point, you completed your first transaction using the read-only wallet mode. In this tutorial, we started with the most basic action with read-only wallet mode, which is starting it in the wallet application, and moved to the most complicated one, which is performing the whole transaction process using a read-only wallet along with a KMS.

Beyond that, performing read-only actions such as consulting balance and checking transaction history work identically to the default wallet mode.

Key takeaways

Finally, keep in mind that the practice we did of Alice transferring X HTR to Bob is the most straightforward transaction we could do using read-only wallet mode. Hathor headless wallet API allows you to use the read-only wallet in the same way to create transactions (and sign them with an external KMS) with any custom token, multiple recipient wallets, and multi-signature wallets.

What's next?