Creating transactions and signing them offline

How to create and sign a transaction in a completely offline environment.

Require the transaction and asset schema

To create, validate and sign transactions, it is necessary to access their schemas. Create a new file schemas.js and add all required schemas in this file, like shown in the snippet below.

In this example, we will send a Token Transfer transaction, therefore we need two different schemas:

  1. the transaction schema (always required)

  2. the Token Transfer asset schema

You can get the corresponding schemas from the module reference pages:

schemas.js
export const transactionSchema = {
     "$id":"lisk/transaction",
     "type":"object",
     "required":[
        "moduleID",
        "assetID",
        "nonce",
        "fee",
        "senderPublicKey",
        "asset"
     ],
     "properties":{
        "moduleID":{
           "dataType":"uint32",
           "fieldNumber":1,
           "minimum":2
        },
        "assetID":{
           "dataType":"uint32",
           "fieldNumber":2
        },
        "nonce":{
           "dataType":"uint64",
           "fieldNumber":3
        },
        "fee":{
           "dataType":"uint64",
           "fieldNumber":4
        },
        "senderPublicKey":{
           "dataType":"bytes",
           "fieldNumber":5,
           "minLength":32,
           "maxLength":32
        },
        "asset":{
           "dataType":"bytes",
           "fieldNumber":6
        },
        "signatures":{
           "type":"array",
           "items":{
              "dataType":"bytes"
           },
           "fieldNumber":7
        }
     }
};

export const transferAssetSchema = {
    $id: 'lisk/transfer-asset',
    title: 'Transfer transaction asset',
    type: 'object',
    required: ['amount', 'recipientAddress', 'data'],
    properties: {
        amount: {
            dataType: 'uint64',
            fieldNumber: 1,
        },
        recipientAddress: {
            dataType: 'bytes',
            fieldNumber: 2,
            minLength: 20,
            maxLength: 20,
        },
        data: {
            dataType: 'string',
            fieldNumber: 3,
            minLength: 0,
            maxLength: 64,
        },
    },
};

Creating and signing the transaction

const { validator, transactions } = require('@liskhq/lisk-client');
const { transactionSchema, transferAssetSchema } = require('./schemas');

// Example account credentials
const account = {
  "passphrase": "chalk story jungle ability catch erupt bridge nurse inmate noodle direct alley",
  "privateKey": "713406a2cf2bdf6b951c1bcba85d44eddbc06d003e8d3faf433b22be28333d97840c66741a76f936bed0a4c308e4f670156e1e1f6b91640bb8d3dd0ae2b3581e",
  "publicKey": "840c66741a76f936bed0a4c308e4f670156e1e1f6b91640bb8d3dd0ae2b3581e",
  "binaryAddress": "85c12d39041bc09e1f89dfeffe4b87cfcfe79fb2",
  "address": "lskuwzrd73pc8z4jnj4sgwgjrjnagnf8nhrovbwdn"
};

// Create the unsigned transaction object manually
const unsignedTransaction = {
  moduleID: Number(2),
  assetID: Number(0), // aka Token Transfer transaction
  fee: BigInt(10000000),
  nonce: BigInt(23),
  senderPublicKey: Buffer.from(account.publicKey,'hex'),
  asset: Buffer.alloc(0),
  signatures: [],
};

// Validate the transaction oject
const transactionErrors = validator.validator.validate(transactionSchema, unsignedTransaction);

if (transactionErrors.length) {
  throw new validator.LiskValidationError([...transactionErrors]);
}

// Create the asset for the Token Transfer transaction
const transferAsset = {
  amount: BigInt(2000000000),
  recipientAddress: Buffer.from(account.binaryAddress,'hex'),
  data: 'Happy birthday!'
};

// Add the transaction asset to the transaction object
unsignedTransaction.asset = transferAsset;

console.log(unsignedTransaction);
/*
{
  moduleID: 2,
  assetID: 0,
  nonce: 1n,
  fee: 10000000n,
  senderPublicKey: <Buffer 84 0c 66 74 1a 76 f9 36 be d0 a4 c3 08 e4 f6 70 15 6e 1e 1f 6b 91 64 0b b8 d3 dd 0a e2 b3 58 1e>,
  asset: {
    amount: 20n,
    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: []
}
*/

// Sign the transaction
const networkIdTestnet = '15f0dacc1060e91818224a94286b13aa04279c640bd5d6f193182031d133df7c';

const signedTransaction = transactions.signTransaction(
  transferAssetSchema,
  unsignedTransaction,
  Buffer.from(networkIdTestnet, 'hex'),
  account.passphrase,
);

console.log(signedTransaction);

/*
{
  moduleID: 2,
  assetID: 0,
  fee: 10000000n,
  nonce: 23n,
  senderPublicKey: <Buffer 84 0c 66 74 1a 76 f9 36 be d0 a4 c3 08 e4 f6 70 15 6e 1e 1f 6b 91 64 0b b8 d3 dd 0a e2 b3 58 1e>,
  asset: {
    amount: 2000000000n,
    recipientAddress: <Buffer 3e 56 5c 6f 2d 22 e0 a3 c1 e4 71 76 72 ec 8a c6 1c 26 60 f2>,
    data: 'Happy birthday!'
  },
  signatures: [
    <Buffer 3c 77 8c e7 b9 8e 72 e6 6b e1 83 86 b4 c1 97 b0 79 3d dc 33 ac ad 8d df 38 d3 52 9f 6a 76 ba 5e 5a ed 54 22 3f b8 36 81 61 b0 2c 71 68 88 3b 09 df b3 ... 14 more bytes>
  ],
  id: <Buffer 95 d2 d3 29 90 cd c7 f3 ae e5 54 b3 f5 23 7b fb f3 4c 33 48 e5 83 72 7a ce dd e5 b3 b6 e3 e7 25>
}
*/

Broadcasting a signed transaction

To finally broadcast the created transaction, use the API client. Adjust the RPC endpoint to point to the node you want to broadcast the transaction to.

Create a file api-client.js which will export the function getClient().

api-client.js
 const { apiClient } = require('@liskhq/lisk-client');

const RPC_ENDPOINT = 'ws://localhost:8080/ws';
let clientCache;

const getClient = async () => {
  if (!clientCache) {
    clientCache = await apiClient.createWSClient(RPC_ENDPOINT);
  }
  return clientCache;
};

module.exports = { getClient };

Import the getClient() function and execute it to use the API client to broadcast the transaction, like shown in below snippet.

const { getClient } = require('./api-client');

// Use the API client to send the transaction to a node
getClient().then(async client => {
  try {
    res = await client.transaction.send(signedTransaction);
    console.log(res);
  } catch (error) {
    console.log(error);
  }
});