Creating transactions and signing them offline
On this page, you’ll learn:
-
How to create and sign a transaction in an entirely offline environment.
-
How to verify the transaction by performing a dry-run.
Option 1: Using the node CLI to create a transaction offline
The CLI of a node can be used to create a transaction, even if the node is currently offline.
The flag The |
./bin/run transaction:create token transfer 10000000 --chain-id 00000000 --nonce 0 --offline
? Please enter: tokenID: 0000000000000000
? Please enter: amount: 1000000000
? Please enter: recipientAddress: lsk44ejwrjb4g44hj9mbtc43h8bcu54g2gbbyyvrk
? Please enter: data: Happy Birthday!
? Please enter passphrase: [hidden]
After all relevant information about the transaction is given, the transaction is created and returned in hex format:
{"transaction":"0802100018022080c2d72f2a20e03c09bdc8c023d94cf66a5d352e6258380210d97d545abbf75668ea3736e3123229088094ebdc031214ab0041a7d3f7b2c290b5b834d46bdc7b7eb858151a0b73656e6420746f6b656e733a40faa2626d7306506b1999f48aa2f4b1ffdee01e641fa76d37a9d1d6fd8c225a81065c856ea625c52d138a7e3ba86b62913dc8e5aef8b5e307641ab66e0277a60b"}
How to additionally return the transaction in JSON format
To also see the decoded transaction object on creation, add the --json
parameter:
$ ./bin/run transaction:create token transfer 10000000 --chain-id 00000000 --nonce 0 --offline --json
This creates a response as seen below:
{
"transaction": "0802100018022080c2d72f2a20e03c09bdc8c023d94cf66a5d352e6258380210d97d545abbf75668ea3736e3123229088094ebdc031214ab0041a7d3f7b2c290b5b834d46bdc7b7eb858151a0b73656e6420746f6b656e733a40faa2626d7306506b1999f48aa2f4b1ffdee01e641fa76d37a9d1d6fd8c225a81065c856ea625c52d138a7e3ba86b62913dc8e5aef8b5e307641ab66e0277a60b"
}
{
"transaction": {
"module": "token",
"command": "transfer",
"fee": "10000000",
"nonce": "0",
"senderPublicKey": "a3f96c50d0446220ef2f98240898515cbba8155730679ca35326d98dcfb680f0",
"signatures": [
"eee00368b7933b6bd06f7ba410261749197b800fc79a816ad15cb3225af6e48cc56c5af1961d6865b8c1ccf9466997e55e8edcf2681c3161a307270bd3d9b800"
],
"params": {
"tokenID": "0400000000000000",
"amount": "10000000",
"recipientAddress": "lskzbqjmwmd32sx8ya56saa4gk7tkco953btm24t8",
"data": "Hey there"
},
"id": "1da484cd297c7654b111987708220e53cb29535ae94276a2fe1486dcfee7e31d"
}
}
Option 2: Creating transactions offline with Lisk Elements
The following Lisk Elements packages are required to create and sign a transaction offline:
-
@liskhq/lisk-client
, including the following libraries:
Provide the transaction schema and params schema
To create, validate, and sign transactions offline with Lisk Elements, it is necessary to access their schemas.
The corresponding param schemas can be found in the module reference pages listed below:
Create a new file schemas.js
and add all required schemas in this file, as shown in the snippet below.
In this example, we will send a Transfer Token transaction, therefore two different schemas are required:
-
The transaction schema (always required).
const transactionSchema = {
$id: '/lisk/transaction',
type: 'object',
required: ['module', 'command', 'nonce', 'fee', 'senderPublicKey', 'params'],
properties: {
module: {
dataType: 'string',
fieldNumber: 1,
minLength: 1,
maxLength: 32,
},
command: {
dataType: 'string',
fieldNumber: 2,
minLength: 1,
maxLength: 32,
},
nonce: {
dataType: 'uint64',
fieldNumber: 3,
},
fee: {
dataType: 'uint64',
fieldNumber: 4,
},
senderPublicKey: {
dataType: 'bytes',
fieldNumber: 5,
minLength: 32,
maxLength: 32,
},
params: {
dataType: 'bytes',
fieldNumber: 6,
},
signatures: {
type: 'array',
items: {
dataType: 'bytes',
},
fieldNumber: 7,
},
},
};
const transferParamsSchema = {
$id: '/lisk/transferParams',
title: 'Transfer transaction params',
type: 'object',
required: ['tokenID', 'amount', 'recipientAddress', 'data'],
properties: {
tokenID: {
dataType: 'bytes',
fieldNumber: 1,
minLength: 8,
maxLength: 8,
},
amount: {
dataType: 'uint64',
fieldNumber: 2,
},
recipientAddress: {
dataType: 'bytes',
fieldNumber: 3,
format: 'lisk32',
},
data: {
dataType: 'string',
fieldNumber: 4,
minLength: 0,
maxLength: 64,
},
},
};
Providing the account credentials
To create and sign the transaction, the credentials of the account sending the transactions are required.
In particular, the following account credentials are required:
-
publicKey
: To create the transaction -
privateKey
: To sign the transaction
Create a file account.json
and add all relevant account credentials into this file.
In this example, we use the following example account credentials:
{
"address": "lskg6prjbqpm6m8rsvmsg6dgyx3e89drknbvxg7x8",
"keyPath": "m/44'/134'/0'",
"publicKey": "ec10255d3e78b2977f04e59ea9afd3e9a2ce9a6b44619ef9f6c47c29695b1df3",
"privateKey": "ac3e34eb369d52a3cddf0bc4312d9b0aa3625b04721039bb114f4c607fb5256eec10255d3e78b2977f04e59ea9afd3e9a2ce9a6b44619ef9f6c47c29695b1df3",
"binaryAddress": "85c12d39041bc09e1f89dfeffe4b87cfcfe79fb2"
}
Creating the transaction
Create a new file create-offline.js
to create the unsigned transaction object.
Define the unsigned transaction object manually by following the transaction schema.
It is recommended to verify the correct format of the transaction with the validator.validate()
function of the @liskhq/lisk-validator
package afterwards.
Then, manually define the parameters for the Transfer Tokens command, and add them to the unsigned transaction.
const { validator } = require('@liskhq/lisk-client');
const { transactionSchema } = require('../schemas');
// Example account credentials
const account = require('./account.json');
const createTxOffline = () => {
// Adjust the values of the unsigned transaction manually
const unsignedTransaction = {
module: "token",
command: "transfer",
fee: BigInt(10000000),
nonce: BigInt(23),
senderPublicKey: Buffer.from(account.publicKey,'hex'),
params: Buffer.alloc(0),
signatures: [],
};
// Validate the transaction
const transactionErrors = validator.validator.validate(transactionSchema, unsignedTransaction);
if (transactionErrors && transactionErrors.length) {
throw new validator.LiskValidationError([...transactionErrors]);
}
// Create the asset for the Token Transfer transaction
const transferParams = {
tokenID: Buffer.from('0000000000000000','hex'),
amount: BigInt(2000000000),
recipientAddress: Buffer.from(account.binaryAddress,'hex'),
data: 'Happy birthday!'
};
// Add the transaction params to the transaction object
unsignedTransaction.params = transferParams;
// Return the unsigned transaction object
return unsignedTransaction;
}
module.exports = { createTxOffline }
The transaction object is now returned, and ready to be signed by the sender in the next step.
Signing the transaction
Create a new file sign-offline.js
to create a script which will sign a given unsigned transaction object.
To sign the transaction, use signTransaction()
function of the @liskhq/lisk-transactions
package.
It requires the following parameters:
-
The unsigned transaction
-
The chain ID
-
The privateKey of the account signing the transaction
-
The params schema for the command addressed in the transaction
const { transactions } = require('@liskhq/lisk-client');
const { transferParamsSchema } = require('../schemas');
const account = require('./account.json');
const chainID = '00000000';
const signTx = (unsignedTransaction) => {
const signedTransaction = transactions.signTransaction(
unsignedTransaction,
Buffer.from(chainID, 'hex'),
Buffer.from(account.privateKey,'hex'),
transferParamsSchema
);
return signedTransaction;
}
module.exports = { signTx }
Verifying the transaction
A transaction dry-run can only be performed online, by connecting to a node.
Without dry-running the transaction, its validity cannot be verified, and the transaction might fail.
To connect to a node, create a function getClient()
which provides an instance of the Lisk API client.
Create a new file api-client.js
and paste the following code:
const { apiClient } = require('@liskhq/lisk-client');
// Adjust the RPC_ENDPOINT to point to your RPC node, if required.
const RPC_ENDPOINT = 'ws://localhost:7887/rpc-ws';
let clientCache;
const getClient = async () => {
if (!clientCache) {
clientCache = await apiClient.createWSClient(RPC_ENDPOINT);
}
return clientCache;
};
module.exports = { getClient };
Create a new file dry-run.js
to create a function that performs a dry-run for a given transaction.
Require the function getClient()
to retrieve the API client, and use it to perform a dry-run of the transaction.
const { getClient } = require('../api-client');
const dryRun = async (signedTransaction) => {
const client = await getClient();
const encTx = client.transaction.encode(signedTransaction);
const result = await client.invoke('txpool_dryRunTransaction', { "transaction": encTx.toString("hex") });
return result;
}
module.exports = { dryRun };
Executing the script
Finally, create a new file index.js
to execute the scripts we defined above one after another.
const { createTxOffline } = require('./create-offline');
const { signTx } = require('./sign-offline');
const { dryRun } = require('./dry-run');
(async () => {
// 1. Create an unsigned transaction
const tx = createTxOffline();
console.log("Unsigned Transaction: ", tx);
// 2. Sign the transaction
const signedTx = signTx(tx);
console.log("Signed Transaction: ", signedTx);
// 3. Perform a dry-run for the signed transaction
const dryRunResult = await dryRun(signedTx)
console.log("Dry-Run Result: ", dryRunResult);
process.exit(0);
})();
An unsigned Transaction looks like this:
{
module: 'token',
command: 'transfer',
fee: 10000000n,
nonce: 23n,
senderPublicKey: <Buffer ec 10 25 5d 3e 78 b2 97 7f 04 e5 9e a9 af d3 e9 a2 ce 9a 6b 44 61 9e f9 f6 c4 7c 29 69 5b 1d f3>,
params: {
tokenID: <Buffer 00 00 00 00 00 00 00 00>,
amount: 2000000000n,
recipientAddress: <Buffer 85 c1 2d 39 04 1b c0 9e 1f 89 df ef fe 4b 87 cf cf e7 9f b2>,
data: 'Happy birthday!'
},
signatures: []
}
Values for the properties signatures
and id
are added to a transaction when it is signed by a user.
{
module: 'token',
command: 'transfer',
fee: 10000000n,
nonce: 23n,
senderPublicKey: <Buffer ec 10 25 5d 3e 78 b2 97 7f 04 e5 9e a9 af d3 e9 a2 ce 9a 6b 44 61 9e f9 f6 c4 7c 29 69 5b 1d f3>,
params: {
tokenID: <Buffer 00 00 00 00 00 00 00 00>,
amount: 2000000000n,
recipientAddress: <Buffer 85 c1 2d 39 04 1b c0 9e 1f 89 df ef fe 4b 87 cf cf e7 9f b2>,
data: 'Happy birthday!'
},
signatures: [
<Buffer cd 28 ce 82 43 67 7b 16 a5 b9 f1 9b 6d 71 58 c0 bb b2 c9 9f 11 dc 25 34 96 d2 6a 14 d4 1c a6 03 d0 ab 49 67 4a 1c ee df aa a7 36 8e d0 1b a3 fa e2 81 ... 14 more bytes>
],
id: <Buffer be 19 fd 1d e0 29 d0 78 e8 03 bb fb b0 8e bc 70 d1 34 e8 c1 d1 37 1f 0a 65 b5 7b 3d 76 9a 45 37>
}
If the dry-run result is 0
, the transaction is valid.
{
result: 0,
events: [
{
data: '0a14fa892e1aa42a8af96c45dfd5afc428b3dba950e612036665651a0803000008000000002080ade2042800',
index: 0,
module: 'token',
name: 'lock',
topics: [Array],
height: 12636
},
{
data: '0a14fa892e1aa42a8af96c45dfd5afc428b3dba950e612036665651a0803000008000000002080ade2042800',
index: 1,
module: 'token',
name: 'unlock',
topics: [Array],
height: 12636
},
{
data: '0a14fa892e1aa42a8af96c45dfd5afc428b3dba950e61208030000080000000018c89cbc022000',
index: 2,
module: 'token',
name: 'burn',
topics: [Array],
height: 12636
},
{
data: '0a14fa892e1aa42a8af96c45dfd5afc428b3dba950e61214821a6aa707422e6d444b42afb2a303d014d620221a08030000080000000020b890a6022800',
index: 3,
module: 'token',
name: 'transfer',
topics: [Array],
height: 12636
},
{
data: '0a14fa892e1aa42a8af96c45dfd5afc428b3dba950e61214821a6aa707422e6d444b42afb2a303d014d6202218c89cbc0220b890a602',
index: 4,
module: 'fee',
name: 'generatorFeeProcessed',
topics: [Array],
height: 12636
},
{
data: '0800',
index: 5,
module: 'token',
name: 'commandExecutionResult',
topics: [Array],
height: 12636
}
]
}