Communicating with a frontend

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.

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 module.

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

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

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

1. Create a new React app

Bootstrap the React app with the following command:

npx create-react-app hello_frontend

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

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

It is already possible to start the frontend at this point:

cd hello_frontend
npm start

2. Install dependencies

To build the frontend, install these two additional dependencies:

npm i react-router-dom (1)
npm i @liskhq/lisk-client (2)
1 Handles the routing between pages.
2 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:

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

3. 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 transaction: A component that allows sending tokens from one account to another.

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

3.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;

3.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 8888.

src/api.js
const { apiClient } = 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;
};

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';
import accounts from '../accounts.json';

// The passphrase for the genesis account of the Devnet.
const accounts = {
  "genesis": {
    "passphrase": "peanut hundred pen hawk invite exclude brain chunk gadget wait wrong ready"
  }
};

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: '',
            },
        }, accounts.genesis.passphrase);
        // 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 && (7)
                <div>
                    <pre>Transaction: {JSON.stringify(state.transaction, null, 2)}</pre>
                    <pre>Response: {JSON.stringify(state.response, null, 2)}</pre>
                </div>
            }
        </div>
    );
};

export default Faucet;

3.3. Send 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 an 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;

3.4. Account details

For the final component, we can add a page that displays the account details by 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;

3.5. 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.

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

4. View in browser

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

npm start

This should open the app in 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.

Homepage

home

New account page

new account

Faucet page

faucet

Transfer tokens

transfer

Get Account details page

account