Creating a user interface

How to build a simple frontend that communicates with a blockchain application built with the Lisk SDK.

To interact with the blockchain application conveniently through a browser, it is possible to build a simple frontend application. This frontend can be built with any technology stack of your choice. In this example, React.js is used.

We will use the @liskhq/lisk-client package in the frontend application to communicate with the blockchain application.

Sample code

View the complete sample code of this guide on GitHub in the Lisk SDK examples repository.

Prerequisites

To use this guide, it is assumed that the following criteria have been met:

  1. Lisk Commander is installed, and a basic blockchain application is already initialized, as explained in the guide Creating a new blockchain application.

  2. A new module HelloModule is created, as explained in the guide Creating a new module.

  3. A new asset HelloAsset is created, as explained in the guide Creating a new asset.

  4. A new plugin LatestHelloPlugin is created, as explained in the guide Creating a new plugin.

  5. The Hello World blockchain application has been configured to successfully connect with the dashboard plugin, as explained in the previous guide How to configure a blockchain application.

1. Update Directory Structure

Create a parent directory HelloApplication.

mkdir HelloApplication
cd HelloApplication

Move your hello_app folder to the HelloApplication.

mv ~/<YOUR_DIRECTORY>/hello_app ~/<YOUR_DIRECTORY>/HelloApplication
├── HelloApplication/
│   └── hello_app/

2. Create a new React app

While being in the HelloApplication folder, bootstrap the React app with the following command:

npx create-react-app@3.4.1 hello_frontend --scripts-version 4.0.3

This will automatically set up a React project for you with default configurations in a newly created hello_frontend folder.

├── HelloApplication
│   ├── hello_app/
│   ├── hello_frontend/
│   │   ├── public/
│   │   ├── src/
│   │   ├── README.md
│   │   └── package.json

It is already possible to start the frontend at this point. It should display the React.js logo in the browser under http://localhost:3000 :

cd hello_frontend
npm start

3. Install dependencies

To build the frontend for the Hello blockchain application, install these two additional dependencies:

npm i react-router-dom@5.3.0 (1)
1 Handles the routing between pages.
npm i @liskhq/lisk-client (1)
1 A collection of Lisk-related libraries which can be used in the frontend.

To use BigInt in the frontend, it may be required to add the following options to the package.json file:

hello_frontend/package.json
{
  // [...]
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ],
    "env": {
      "es2020": true,
      "browser": true,
      "node": true,
      "mocha": true
    }
  },
  // [...]
}

After updating the package.json file, install the dependencies in the hello_frontend:

npm install

4. Create basic components

This simple app can be customized by creating different components for the first basic functions of the frontend as shown below:

  1. New account: Generates new account credentials.

  2. Faucet: A component that sends funds to a specified account from the genesis account.

  3. Send Transfer transaction: A component that allows sending tokens from one account to another.

  4. Account details: Returns details of a user account by address.

4.1. New account

A page for generating new accounts that conveniently allows the creation of credentials that can be used in the application.

Import passphrase and cryptography from the lisk-client package to create new account credentials.

src/components/NewAccount.js
import React, { Component } from 'react';
import { passphrase, cryptography } from '@liskhq/lisk-client';

const newCredentials = () => {
    const pass = passphrase.Mnemonic.generateMnemonic();
    const keys = cryptography.getPrivateAndPublicKeyFromPassphrase(pass);
    const credentials = {
        address: cryptography.getBase32AddressFromPassphrase(pass),
        binaryAddress: cryptography.getAddressFromPassphrase(pass).toString("hex"),
        passphrase: pass,
        publicKey: keys.publicKey.toString("hex"),
        privateKey: keys.privateKey.toString("hex")
    };
    return credentials;
};

const NewAccount = () => {
    const credentials = newCredentials();
    return (
        <div>
            <h2>Create new account</h2>
            <p>Refresh page to get new credentials.</p>
            <pre>{JSON.stringify(credentials, null, 2)}</pre>
        </div>
    );
}
export default NewAccount;

4.2. Faucet

The faucet is a component that allows accounts to receive tokens from the genesis account, which holds the majority of initial tokens at the start of the Devnet.

In a new file api.js, the apiClient from package lisk-client provides an interface for the faucet and other React components to connect to the blockchain application via a WebSocket on port 8080.

src/api.js
const { apiClient, cryptography } = require('@liskhq/lisk-client');
const RPC_ENDPOINT = 'ws://localhost:8080/ws';

let clientCache;

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

export const fetchAccountInfo = async (address) => {
    const client = await getClient();
    return client.account.get(cryptography.getAddressFromBase32Address(address));
};

export const fetchHelloCounter = async () => {
    const client = await getClient();
    return client.invoke('hello:amountOfHellos');
};

export const fetchLatestHello = async () => {
    const client = await getClient();
    return client.invoke('latestHello:getLatestHello');
};

Next, create a new file Faucet.js, which will store the React component of the faucet.

src/components/Faucet.js
import React, { useState } from 'react';
// `transactions` and `cryptography` from the `lisk-client` package are used to convert the data of the transaction into the correct format.
import { cryptography, transactions } from '@liskhq/lisk-client';
// Inside `Faucet.js`, import the previously defined API client from `api.js`.
import * as api from '../api.js';
//List of accounts, the list should be same as the hello_app
import accounts from '../accounts.json';

const Faucet = () => {
    const [state, updateState] = useState({
        address: '',
        amount: '',
        transaction: {},
        response: {}
    });

    const handleChange = (event) => {
        const { name, value } = event.target;
        updateState({
            ...state,
            [name]: value,
        });
    };

    const handleSubmit = async (event) => {
        event.preventDefault();

        const client = await api.getClient();
        const address = cryptography.getAddressFromBase32Address(state.address);
        // The API client is used to create the transaction object based on the inputs in the form below.
        const tx = await client.transaction.create({
            moduleID: 2,
            assetID: 0,
            fee: BigInt(transactions.convertLSKToBeddows('0.01')),
            asset: {
                amount: BigInt(transactions.convertLSKToBeddows(state.amount)),
                recipientAddress: address,
                data: '',
            },
        }, JSON.parse(JSON.stringify(accounts[10]["passphrase"]))); // Address of a delegate account
        // After creation, the transaction is submitted to the blockchain application.
        const response = await client.transaction.send(tx);
        // After submitting the transaction and receiving the response, the state of the Faucet component is updated with the transaction object and the API response.
        updateState({
            transaction: client.transaction.toJSON(tx),
            address: '',
            amount: '',
            response: response
        });
    }

    return (
        <div>
            <h2>Faucet</h2>
            <p>The faucet transfers tokens from the genesis account to another.</p>
            <form onSubmit={handleSubmit}>
                <label>
                    Address:
                    <input type="text" id="address" name="address" onChange={handleChange} value={state.address} />
                </label>
                <label>
                    Amount (1 = 10^8 tokens):
                    <input type="text" id="amount" name="amount" onChange={handleChange} value={state.amount} />
                </label>
                <input type="submit" value="Submit" />
            </form>
            {state.transaction &&
                <div>
                    <pre>Transaction: {JSON.stringify(state.transaction, null, 2)}</pre>
                    <pre>Response: {JSON.stringify(state.response, null, 2)}</pre>
                </div>
            }
        </div>
    );
};

export default Faucet;

Both frontend and blockchain applications should have the same accounts.json. So, copy accounts.json from hello_app and paste it into hello_frontend.

cp hello_app/config/default/accounts.json hello_frontend/src/

4.3. Send Transfer transaction

Now that it is possible to create a new account and receive some initial tokens, we can build a new component that allows the possibility to be able to send tokens from one account to another.

To do this, create a new file Transfer.js. The contents of Transfer.js are similar to Faucet.js, as a transfer transaction will be sent on both pages. The only difference is that the sender is not essentially a genesis account, but can be any account in the network.

src/components/Transfer.js
import React, { useState } from 'react';
import { cryptography, transactions } from '@liskhq/lisk-client';
import * as api from '../api.js';

const Transfer = () => {
    const [state, updateState] = useState({
        address: '',
        amount: '',
        fee: '',
        passphrase: '',
        transaction: {},
        response: {}
    });

    const handleChange = (event) => {
        const { name, value } = event.target;
        updateState({
            ...state,
            [name]: value,
        });
    };

    const handleSubmit = async (event) => {
        event.preventDefault();

        const client = await api.getClient();
        const address = cryptography.getAddressFromBase32Address(state.address);
        // Here the transaction gets signed with the passphrase provided in the form.
        const tx = await client.transaction.create({
            moduleID: 2,
            assetID: 0,
            fee: BigInt(transactions.convertLSKToBeddows(state.fee)),
            asset: {
                amount: BigInt(transactions.convertLSKToBeddows(state.amount)),
                recipientAddress: address,
                data: '',
            },
        }, state.passphrase);
        let res;
        try {
            res = await client.transaction.send(tx);
        } catch (error) {
            res = error;
        }

        updateState({
            transaction: client.transaction.toJSON(tx),
            response: res,
            address: '',
            amount: '',
            fee: '',
            passphrase: '',
        });
    };

    return (
        <div>
            <h2>Transfer</h2>
            <p>Send tokens from one account to another.</p>
            <form onSubmit={handleSubmit}>
                <label>
                    Recipient:
                        <input type="text" id="address" name="address" onChange={handleChange} value={state.address} />
                </label>
                <label>
                    Amount (1 = 10^8 tokens):
                        <input type="text" id="amount" name="amount" onChange={handleChange} value={state.amount} />
                </label>
                <label>
                    Fee:
                        <input type="text" id="fee" name="fee" onChange={handleChange}  value={state.fee} />
                </label>
                <label>
                    Passphrase:
                        <input type="text" id="passphrase" name="passphrase" onChange={handleChange}  value={state.passphrase} />
                </label>
                <input type="submit" value="Submit" />
            </form>
            {state.transaction &&
                <div>
                    <pre>Transaction: {JSON.stringify(state.transaction, null, 2)}</pre>
                    <pre>Response: {JSON.stringify(state.response, null, 2)}</pre>
                </div>
            }
        </div>
    );
}
export default Transfer;

4.4. Send Hello transaction

Now that we defined a component for posting one of the default transaction types, the Transfer transaction, create another component for posting the custom Hello transaction, which was implemented before in the guide Creating a new asset.

Create a new file Hello.js inside of the components/ folder, and add the following code to it:

src/components/Hello.js
import { cryptography, transactions } from '@liskhq/lisk-client';
import React, { Component, useState } from 'react';
import * as api from '../api.js';

const Hello = () => {
    const [state, updateState] = useState({
        hello: '',
        fee: '',
        passphrase: '',
        transaction: {},
        response: {}
    });

    const handleChange = (event) => {
        const { name, value } = event.target;
        updateState({
            ...state,
            [name]: value,
        });
    };

    const handleSubmit = async (event) => {
        event.preventDefault();

        const client = await api.getClient();
        const tx = await client.transaction.create({
            moduleID: 1000,
            assetID: 0,
            fee: BigInt(transactions.convertLSKToBeddows(state.fee)),
            asset: {
                helloString: state.hello,
            },
        }, state.passphrase);

        let res = '';
        try {
          res = await client.transaction.send(tx);
        } catch (error) {
          res = error;
        }
        updateState({
          transaction: client.transaction.toJSON(tx),
          response: res,
          hello: '',
          fee: '',
          passphrase: '',
        });
    };

    return (
        <div>
            <h2>Hello</h2>
            <p>Send a Hello transaction.</p>
            <form onSubmit={handleSubmit}>
                <label>
                    Hello message:
                        <input type="text" id="hello" name="hello" onChange={handleChange} value={state.hello} />
                </label>
                <label>
                    Fee:
                        <input type="text" id="fee" name="fee" onChange={handleChange} value={state.fee} />
                </label>
                <label>
                    Passphrase:
                        <input type="text" id="passphrase" name="passphrase" onChange={handleChange} value={state.passphrase} />
                </label>
                <input type="submit" value="Submit" />
            </form>
            <div>
                <pre>Transaction: {JSON.stringify(state.transaction, null, 2)}</pre>
                <pre>Response: {JSON.stringify(state.response, null, 2)}</pre>
            </div>
        </div>
    );
}
export default Hello;

4.5. Account details

For the final component, it is possible to add a page that displays the account details by the address.

The API client is imported again from api.js, in order to communicate with the blockchain application.

src/components/Account.js
import { cryptography } from '@liskhq/lisk-client';
import React, { useState } from 'react';
import * as api from '../api.js';

const Account = () => {
    const [state, updateState] = useState({
        address: '',
        account: {},
    });

    const handleChange = (event) => {
        const { name, value } = event.target;
        updateState({
            ...state,
            [name]: value,
        });
    };

    const handleSubmit = async (event) => {
        event.preventDefault();
        const client = await api.getClient();
        // Retrieves the account details from the blockchain application, based on the address provided.
        const account = await client.account.get(cryptography.getAddressFromBase32Address(state.address));
        updateState({
            ...state,
            account: client.account.toJSON(account),
        });
    };

    return (
        <div>
            <h2>Account</h2>
            <p>Get account details by address.</p>
            <form onSubmit={handleSubmit}>
                <label>
                    Address:
                        <input type="text" id="address" name="address" onChange={handleChange} value={state.address} />
                </label>
                <input type="submit" value="Submit" />
            </form>
            <div>
                <pre>Account: {JSON.stringify(state.account, null, 2)}</pre>
            </div>
        </div>
    );
}
export default Account;

4.6. Index and navigation

Now that all the basic components for the frontend are created, a small component for the landing page can be added.

src/components/Home.js
import React, { Component } from 'react';
import { fetchHelloCounter, fetchLatestHello } from '../api.js';

class Home extends Component {

    constructor(props) {
      super(props);

      this.state = {
        data: {
          helloCounter: 0,
        },
        latestHello: {
          message: 'N/A',
          sender: 'N/A'
        }
      };
    }

    async componentDidMount() {
        const helloData = await fetchHelloCounter() ;
        const latestHello = await fetchLatestHello() ;

        this.setState({
          data: {
            helloCounter: helloData.helloCounter
          },
          latestHello: {
            message: latestHello ? latestHello.hello: '',
            sender: latestHello ? latestHello.sender : '',
        }});
    }

    render() {
        return (
            <div>
                <h2>Hello Lisk!</h2>
                <p>A simple frontend for blockchain applications built with the Lisk SDK.</p>
                <p>Hello counter:</p>
                <pre>{this.state.data.helloCounter}</pre>
                <p>Latest Hello:</p>
                <p>Message:</p>
                <pre>{this.state.latestHello.message}</pre>
                <p>Sender:</p>
                <pre>{this.state.latestHello.sender}</pre>
            </div>
        );
    }
}

export default Home;

Move the file App.js into the src/components/ folder. Now update the file to include the above defined React components and build a basic navigation structure.

src/components/App.js
import React from "react";
import {
    BrowserRouter as Router,
    Switch,
    Route,
    Link
} from "react-router-dom";
import "regenerator-runtime/runtime.js";
import Home from './Home';
import NewAccount from './NewAccount';
import Faucet from './Faucet';
import SendHello from './Hello';
import Account from './Account';
import Transfer from './Transfer';

export const app = () => {
    return (
        <Router>
            <div>
                <Route>
                    <ul>
                        <li><Link to="/">Home</Link></li>
                        <hr />
                        <h3> Interact </h3>
                        <li><Link to="/new-account">New Account</Link></li>
                        <li><Link to="/faucet">Faucet</Link></li>
                        <li><Link to="/send-hello">Send Hello</Link></li>
                        <li><Link to="/send-transfer">Send Transfer</Link></li>
                        <hr />
                        <h3> Explore </h3>
                        <li><Link to="/account">Account</Link></li>
                    </ul>
                </Route>

                <Switch>
                    <Route exact path="/">
                        <Home />
                    </Route>
                    <Route path="/send-hello">
                        <SendHello />
                    </Route>
                    <Route path="/new-account">
                        <NewAccount />
                    </Route>
                    <Route path="/faucet">
                        <Faucet />
                    </Route>
                    <Route path="/send-transfer">
                        <Transfer />
                    </Route>
                    <Route path="/account">
                        <Account />
                    </Route>
                </Switch>
            </div>
        </Router>
    );
}

export default app;

In the already existing index.js file, the App.js component is finally included in the root element, which is defined in index.html.

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

5. Update Configuration

A WebSocket connection defined in the api.js must also be configured in the hello_app. So, add the following configuration to the config.json file.

hello_app/config/default/config.json
{
  // [...]
  "rpc": {
    "enable": true,
    "mode": "ws",
    "port": 8080,
    "host": "127.0.0.1"
  },
  // [...]
}

For the new configurations to take effect, the existing data of hello_app must be removed:

rm -rf ~/.lisk

6. View in browser

After completing all the steps above, start the app again:

npm start

This should open the app in the browser under the URL http://localhost:3000 .

It is also necessary to start the corresponding blockchain application if it is not running already.

It is now possible to use the app in a browser to create new accounts, fund accounts, view the account details of a specific account, and send tokens from one account to another as shown below.

home
Homepage
new account
New account page
faucet
Faucet page
transfer
Transfer tokens
account
Get Account details page