Creating transactions and signing them offline

Creating transactions

Creating transactions with lisk-client

Require the transaction and asset schema

To create, validate, and sign transactions offline with the lisk-client, it is necessary to access their schemas. 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 Token Transfer transaction, therefore two different schemas are required:

  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>
}
*/

Using the CLI to create a transaction

The CLI of a node can be used to create a sendable transaction object.

The flag --offline is used here, so that the transaction can be signed, even if the node is not connected to any network at the moment.

The --offline flag also requires the flags --network-identifier and --nonce to be specified.

$ ./bin/run transaction:create 2 0 100000000 \
--network-identifier=15f0dacc1060e91818224a94286b13aa04279c640bd5d6f193182031d133df7c \
--nonce=2 --offline
? Please enter: amount:  1000000000
? Please enter: recipientAddress:  ab0041a7d3f7b2c290b5b834d46bdc7b7eb85815
? Please enter: data:  send tokens
? Please enter passphrase:  [hidden]
? Please re-enter passphrase:  [hidden]

After all relevant information about the transaction is given, the already encoded transaction object is returned:

{"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 2 0 100000000 \
--network-identifier=15f0dacc1060e91818224a94286b13aa04279c640bd5d6f193182031d133df7c \
--nonce=2 --offline --json --pretty
? Please enter: amount:  1000000000
? Please enter: recipientAddress:  ab0041a7d3f7b2c290b5b834d46bdc7b7eb85815
? Please enter: data:  send tokens
? Please enter passphrase:  [hidden]
? Please re-enter passphrase:  [hidden]
{
  "transaction": "0802100018022080c2d72f2a20e03c09bdc8c023d94cf66a5d352e6258380210d97d545abbf75668ea3736e3123229088094ebdc031214ab0041a7d3f7b2c290b5b834d46bdc7b7eb858151a0b73656e6420746f6b656e733a40faa2626d7306506b1999f48aa2f4b1ffdee01e641fa76d37a9d1d6fd8c225a81065c856ea625c52d138a7e3ba86b62913dc8e5aef8b5e307641ab66e0277a60b"
}
{
  "transaction": {
    "moduleID": 2,
    "assetID": 0,
    "nonce": "2",
    "fee": "100000000",
    "senderPublicKey": "e03c09bdc8c023d94cf66a5d352e6258380210d97d545abbf75668ea3736e312",
    "signatures": [
      "faa2626d7306506b1999f48aa2f4b1ffdee01e641fa76d37a9d1d6fd8c225a81065c856ea625c52d138a7e3ba86b62913dc8e5aef8b5e307641ab66e0277a60b"
    ],
    "asset": {
      "amount": "1000000000",
      "recipientAddress": "ab0041a7d3f7b2c290b5b834d46bdc7b7eb85815",
      "data": "send tokens"
    }
  }
}

Broadcasting/sending a signed transaction

A transaction can be posted to a node in the following ways:

If the existing data needs to be encoded/decoded before posting, check the guide Decoding & encoding transactions and blocks with the API Client

Via API client

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

Adjust the RPC (Remote-Procedure-Call), endpoint to point to the node you want to broadcast the transaction to.

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);
  }
});

Via RPC API

If you prefer to use the RPC WebSocket API of Lisk Service to post transactions, this can be achieved for example by writing a small JS script, and using the API client of the socket.io-client package:

// 1. Require the dependencies
const io = require('socket.io-client'); // The socket.io client
const jsome = require('jsome'); // Prettifies the JSON output

jsome.params.colored = true;

// Use local Service node
const WS_RPC_ENDPOINT = 'ws://localhost:9901/rpc-v2';
//Use public Service node
//const WS_RPC_ENDPOINT = "wss://service.lisk.com/rpc-v2";

// 2. Connect to Lisk Service via WebSockets
const socket = io(WS_RPC_ENDPOINT, {
  forceNew: true,
  transports: ['websocket']
});

// 3. Emit the remote procedure call
socket.emit('request', {
  jsonrpc: '2.0',
  method: 'post.transactions',
  payload: {"transaction":"08021000180d2080c2d72f2a200fe9a3f1a21b5530f27f87a414b549e79a940bf24fdf2b2f05e7f22aeeecc86a32270880c2d72f12144fd8cc4e27a3489b57ed986efe3d327d3de40d921a0a73656e6420746f6b656e3a4069242925e0e377906364fe6c2eed67f419dfc1a757f73e848ff2f1ff97477f90263487d20aedf538edffe2ce5b3e7601a8528e5cd63845272e9d79c294a6590a"}
},
  answer => {
    // console.log(answer);
    jsome(answer);
    process.exit(0);
});

Via CLI

$ lisk-core transaction:send 0802100018022080c2d72f2a20e03c09bdc8c023d94cf66a5d352e6258380210d97d545abbf75668ea3736e3123229088094ebdc031214ab0041a7d3f7b2c290b5b834d46bdc7b7eb858151a0b73656e6420746f6b656e733a40faa2626d7306506b1999f48aa2f4b1ffdee01e641fa76d37a9d1d6fd8c225a81065c856ea625c52d138a7e3ba86b62913dc8e5aef8b5e307641ab66e0277a60b

Via Lisk Service HTTP API

Lisk Service needs to be installed in order to post a transaction via the Lisk Service HTTP API.

cURL is one of the tools that can be used to send HTTP API requests to Lisk Service as shown below:

curl -X POST -H "Content-Type: application/json" \
-d '{ "transaction": "0802100018022080c2d72f2a20e03c09bdc8c023d94cf66a5d352e6258380210d97d545abbf75668ea3736e3123229088094ebdc031214ab0041a7d3f7b2c290b5b834d46bdc7b7eb858151a0b73656e6420746f6b656e733a40faa2626d7306506b1999f48aa2f4b1ffdee01e641fa76d37a9d1d6fd8c225a81065c856ea625c52d138a7e3ba86b62913dc8e5aef8b5e307641ab66e0277a60b"}' \
"http://localhost:9901/api/v2/transactions"

The following response will be displayed if the transaction was posted successfully:

{"message":"Transaction payload was successfully passed to the network node","transactionId":"8a503843942e7d47ba0bef83fe735d26381f32a6ca6c96fb1cde902315f6220c"}

Via Lisk Core HTTP API

The HTTP API plugin needs to be enabled on the node in order to post a transaction via the node HTTP API.

cURL is one of the tools that can be used to send HTTP API requests to Lisk Core as shown below:

curl -X POST -H "Content-Type: application/json" \
-d '{ "transaction": "0802100018022080c2d72f2a20e03c09bdc8c023d94cf66a5d352e6258380210d97d545abbf75668ea3736e3123229088094ebdc031214ab0041a7d3f7b2c290b5b834d46bdc7b7eb858151a0b73656e6420746f6b656e733a40faa2626d7306506b1999f48aa2f4b1ffdee01e641fa76d37a9d1d6fd8c225a81065c856ea625c52d138a7e3ba86b62913dc8e5aef8b5e307641ab66e0277a60b"}' \
"http://localhost:4000/api/transactions"

The following response will be displayed if the transaction was posted successfully:

{"message":"Transaction payload was successfully passed to the network node","transactionId":"8a503843942e7d47ba0bef83fe735d26381f32a6ca6c96fb1cde902315f6220c"}