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 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:
- Generate Alice's extended public key.
- Start Alice's wallet in read-only mode.
- Create a transaction proposal.
- Obtain inputs information.
- Generate inputs signatures.
- Generate signed inputs data.
- Append signed inputs data into transaction proposal.
- 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.
<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.
- Using source code
- Using Docker
If you run Hathor headless wallet from source code, use the following substeps to generate the extended public key:
- Open the command line from the directory where you installed Hathor headless wallet.
- 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>"
If you run Hathor headless wallet using Docker, generate the extended public key, replacing the <alice_24_words_seed_phrase>
placeholder with Alice's wallet seed phrase:
docker run --rm --entrypoint /bin/sh \
hathornetwork/hathor-wallet-headless \
-c "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>
:
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.
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.
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:
{
"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:
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:
{
"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:
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.
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.
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:
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:
{
"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:
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:
{
"success": true,
"txHex": "<signed_tx_proposal>"
}
Step 8: push signed transaction proposal
Finally, Alice will push the signed transaction proposal to Hathor Network:
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:
{
"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?
-
About the read-only wallet mode: to understand the full potential of read-only wallets.
-
Functioning of read-only wallets for integrated systems: for developers to understand the conceptual implementation of the read-only wallets feature into their wallet composite systems.
-
Hathor headless wallet HTTP API reference: for developers to consult while implementing read-only wallets into their use cases.