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 00000001 --nonce 0 --offline
? Please enter: tokenID: 0000000100000000
? Please enter: amount: 1000000000
? Please enter: recipientAddress: klyg6prjbqpm6m8rsvmsg6dgyx3e89drknbvxg7x8
? 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":"0a05746f6b656e12087472616e7366657218002080ade2042a20eef0f24cc71fe2bd48a2b8c10b9a44fa304674069230a25b11ee82d8638ab363322d0a0800000001000000001080c8afa0251a14fa892e1aa42a8af96c45dfd5afc428b3dba950e6220548656c6c6f3a40c536e178e4000c6631f099f7f7af3ea12d89fef0b11e8ac5cb9ff025283849a8ab9fa03ac8542dd36f5eda8af48a279569d67943dc9faf385040d03375ecc201"}
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 00000001 --nonce 0 --offline --json
This creates a response as seen below:
{
"transaction": "0a05746f6b656e12087472616e7366657218002080ade2042a20eef0f24cc71fe2bd48a2b8c10b9a44fa304674069230a25b11ee82d8638ab363322d0a0800000001000000001080c8afa0251a14fa892e1aa42a8af96c45dfd5afc428b3dba950e6220548656c6c6f3a40c536e178e4000c6631f099f7f7af3ea12d89fef0b11e8ac5cb9ff025283849a8ab9fa03ac8542dd36f5eda8af48a279569d67943dc9faf385040d03375ecc201"
}
{
"transaction": {
"module": "token",
"command": "transfer",
"fee": "10000000",
"nonce": "0",
"senderPublicKey": "eef0f24cc71fe2bd48a2b8c10b9a44fa304674069230a25b11ee82d8638ab363",
"signatures": [
"c536e178e4000c6631f099f7f7af3ea12d89fef0b11e8ac5cb9ff025283849a8ab9fa03ac8542dd36f5eda8af48a279569d67943dc9faf385040d03375ecc201"
],
"params": {
"tokenID": "0000000100000000",
"amount": "10000000000",
"recipientAddress": "klyg6prjbqpm6m8rsvmsg6dgyx3e89drknbvxg7x8",
"data": "Hello"
},
"id": "a01f5a6e5e753a872652cbefc1578bcf90b99a89cbfc522b4afded53ce344cbc"
}
}
Option 2: Creating transactions offline with Klayr Elements
The relevant files discussed in this guide are schemas.js, account.json, create-offline.js, sign-offline.js, api-client.js, dry-run.js and index.js. |
The following Klayr Elements packages are required to create and sign a transaction offline:
-
@klayr/client
, including the following libraries:-
@klayr/validator (optional)
Provide the transaction schema and params schema
To create, validate, and sign transactions offline with Klayr 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: '/klayr/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: '/klayr/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: 'klayr32',
},
data: {
dataType: 'string',
fieldNumber: 4,
minLength: 0,
maxLength: 64,
},
},
};
module.exports = { transferParamsSchema, transactionSchema };
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.
You can also create a new account using the keys:create CLI command.
In this example, we use the following example account credentials:
{
"address": "klyg6prjbqpm6m8rsvmsg6dgyx3e89drknbvxg7x8",
"keyPath": "m/44'/134'/0'",
"publicKey": "ec10255d3e78b2977f04e59ea9afd3e9a2ce9a6b44619ef9f6c47c29695b1df3",
"privateKey": "ac3e34eb369d52a3cddf0bc4312d9b0aa3625b04721039bb114f4c607fb5256eec10255d3e78b2977f04e59ea9afd3e9a2ce9a6b44619ef9f6c47c29695b1df3",
"binaryAddress": "fa892e1aa42a8af96c45dfd5afc428b3dba950e6"
}
Retrieving binaryAddress
Each account’s credential contains an address
in the Klayr32 format.
To convert an address into a binary string format, you can use klayr-console
.
-
Start a Klayr console session.
klayr-console
-
Pass the Klayr32 address to the
getAddressFromKlayr32Address
function:klayr.cryptography.address.getAddressFromKlayr32Address('klyg6prjbqpm6m8rsvmsg6dgyx3e89drknbvxg7x8').toString('hex')
-
The console will output the resultant binary address, as shown below:
'fa892e1aa42a8af96c45dfd5afc428b3dba950e6'
For all the available conversions, please refer to the klayr.cryptography.address package.
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 @klayr/validator
package afterwards.
Then, manually define the parameters for the Token Transfer command, and add them to the unsigned transaction.
const { validator } = require('@klayr/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
validator.validator.validate(transactionSchema, unsignedTransaction);
// Create the asset for the Token Transfer transaction
const transferParams = {
tokenID: Buffer.from('0000000100000000', 'hex'),
amount: BigInt(1000),
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 }
For an offline transaction to dry-run successfully, the tokenID and chainID of the node must be the same as the ones used in the creation of an offline transaction.
|
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 that will sign a given unsigned transaction object.
To sign the transaction, use the signTransaction()
function of the @klayr/transactions
package.
It requires the following parameters:
-
The unsigned transaction
-
The chain ID
-
The private key of the account signing the transaction
-
The params schema for the command addressed in the transaction
const { transactions } = require('@klayr/client');
const { transferParamsSchema } = require('./schemas');
const account = require('./account.json');
const chainID = '00000001';
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 Klayr API client.
Create a new file api-client.js
and paste the following code:
const { apiClient } = require('@klayr/client');
const RPC_ENDPOINT = 'ws://127.0.0.1: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:
Unsigned Transaction: {
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 01 00 00 00 00>,
amount: 1000n,
recipientAddress: <Buffer fa 89 2e 1a a4 2a 8a f9 6c 45 df d5 af c4 28 b3 db a9 50 e6>,
data: 'Happy birthday!'
},
signatures: []
}
Values for the properties signatures
and id
are added to a transaction when it is signed by a user.
Signed Transaction: {
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 01 00 00 00 00>,
amount: 1000n,
recipientAddress: <Buffer fa 89 2e 1a a4 2a 8a f9 6c 45 df d5 af c4 28 b3 db a9 50 e6>,
data: 'Happy birthday!'
},
signatures: [
<Buffer 64 08 a2 9d 7f 39 55 ed 5e 47 9f a6 90 b1 c2 61 8f 07 ab cc 70 bd 10 05 44 2f 89 b5 74 9f b7 b5 16 1d 73 db 79 9e ab e7 07 7e f5 40 bd e3 91 de 99 33 ... 14 more bytes>
],
id: <Buffer ea 7e a3 a8 dd bf 9f 88 0a da eb 17 5a 47 d5 b8 bf 70 39 80 09 63 66 a7 be 7f 9d eb 01 43 73 b3>
}
If the dry-run result is 1
, the transaction is valid.
Dry-Run Result: {
result: 1,
events: [
{
data: '0a14fa892e1aa42a8af96c45dfd5afc428b3dba950e612036665651a0800000001000000002080ade2042800',
index: 0,
module: 'token',
name: 'lock',
topics: [Array],
height: 2940
},
{
data: '0a14fa892e1aa42a8af96c45dfd5afc428b3dba950e61214fa892e1aa42a8af96c45dfd5afc428b3dba950e61a08000000010000000020e8072800',
index: 1,
module: 'token',
name: 'transfer',
topics: [Array],
height: 2940
},
{
data: '0a14fa892e1aa42a8af96c45dfd5afc428b3dba950e612036665651a0800000001000000002080ade2042800',
index: 2,
module: 'token',
name: 'unlock',
topics: [Array],
height: 2940
},
{
data: '0a14fa892e1aa42a8af96c45dfd5afc428b3dba950e6121403cb3daae6009976ebac3b8935444bc3677b68821a08000000010000000020b0bed7042800',
index: 3,
module: 'token',
name: 'transfer',
topics: [Array],
height: 2940
},
{
data: '0a14fa892e1aa42a8af96c45dfd5afc428b3dba950e61208000000010000000018d0ee0a2000',
index: 4,
module: 'token',
name: 'burn',
topics: [Array],
height: 2940
},
{
data: '0a14fa892e1aa42a8af96c45dfd5afc428b3dba950e6121403cb3daae6009976ebac3b8935444bc3677b688218d0ee0a20b0bed704',
index: 5,
module: 'fee',
name: 'generatorFeeProcessed',
topics: [Array],
height: 2940
},
{
data: '0801',
index: 6,
module: 'token',
name: 'commandExecutionResult',
topics: [Array],
height: 2940
}
]
}