Create Publication

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Solution Thumbnail

Algorand Asset Management Portal

Overview

The Algorand protocol supports the creation of on-chain assets that benefit from the same security, compatibility, speed and ease of use as the Algo. The official name for assets on Algorand is Algorand Standard Assets (ASA). You can learn more about Algorand Standard Assets in the docs.

In this solution, we develop an application with an intuitive user interface where an end user can seamlessly create, configure, destroy, freeze and unfreeze the assets for an account on the Algorand blockchain.

Before we move on to technical stuff, you can try out the application here to get a idea of the end result:
http://algodesk.io/

Set Up

Here is the technology stack for the application:

  1. React JS for front-end.( https://reactjs.org/ )
  2. Redux for state management ( https://daveceddia.com/redux-tutorial/ )
  3. Algorand JS SDK ( https://github.com/algorand/js-algorand-sdk )
  4. Material UI for React JS ( https://material-ui.com/getting-started/installation/ )
  5. Webpack for module bundling ( https://webpack.js.org/ )
  6. Babel as ES6 compiler ( https://babeljs.io/ )
  7. Purestake API for interacting with the Algorand network ( https://developer.purestake.io/ )

To set up a basic React app and configure it with Webpack and Babel follow this tutorial:
https://blog.usejournal.com/setting-up-react-webpack-4-babel-7-from-scratch-2019-b771dca2f637

Application Structure

We divide our application into 2 pages with the following components:

  1. Login page
    • Login Component
  2. Dashboard page
    • Home Component
    • Create Asset Component
    • Modify Asset Component

How to Use Algorand JS SDK

I have created a service class, which is basically a wrapper around the js-algorand-sdk.
Make sure you have a PureStake account to get your own API key: https://developer.purestake.io/.

This service class has all the functions required to manage the assets, including getting the wallet and transaction details. Every interaction with the Algorand blockchain has to go through here so import this service whereever it is required to use the API’s which are defined in the SDK.

List of APIs available in this service class are:

  • Get account details
  • Get Asset information
  • Create Asset
  • Modify Asset
  • Destroy Asset
  • Freeze Assets
  • Unfreeze Assets
  • Revoke Assets
  • Transfer Assets
  • Wait until a transaction is confirmed
  • Get transaction details after confirmation

We will be using this service through out the application.

import sdk from 'algosdk';

const token = {
    'X-API-Key': '<PureStake API Key>'
};
const port = '';

class AlgoSdk {

    client;
    network;
    networks = [{
        name: 'testnet',
        server: 'https://testnet-algorand.api.purestake.io/ps1',
        label: 'TESTNET',
        explorer: 'https://goalseeker.purestake.io/algorand/testnet'
    }, {
        name: 'mainnet',
        server: 'https://mainnet-algorand.api.purestake.io/ps1',
        label: 'MAINNET',
        explorer: 'https://goalseeker.purestake.io/algorand/mainnet'
    }];

    constructor() {
        this.selectNetwork('testnet');
    }

    selectNetwork(name) {
        const networks = this.getNetworks();
        networks.forEach((network) => {
            if (network.name === name) {
                this.network = network;
                this.setClient(network.server);
            }
        })
    }

    getExplorer() {
        const network = this.getCurrentNetwork();
        return this.network.explorer;
    }

    getAssetUrl(id) {
       return this.getExplorer() + '/asset/' + id;
    }

    getCurrentNetwork() {
        return this.network;
    }

    setClient(server) {
        this.client = new sdk.Algod(token, server, port);
    }

    getClient() {
        return this.client;
    }

    mnemonicToSecretKey(mnemonic) {
        return sdk.mnemonicToSecretKey(mnemonic);
    }

    async getAccountInformation(address) {
        return await this.getClient().accountInformation(address);
    }

    async getAssetInformation(assetID) {
        return  await this.getClient().assetInformation(assetID);
    }

    async getChangingParams() {
        const cp = {
            fee: 0,
            firstRound: 0,
            lastRound: 0,
            genID: "",
            genHash: ""
        };

        let params = await this.getClient().getTransactionParams();
        cp.firstRound = params.lastRound;
        cp.lastRound = cp.firstRound + parseInt(1000);
        let sFee = await this.getClient().suggestedFee();
        cp.fee = sFee.fee;
        cp.genID = params.genesisID;
        cp.genHash = params.genesishashb64;

        return cp;
    }

    async waitForConfirmation(txId) {
        let lastRound = (await this.getClient().status()).lastRound;
        while (true) {
            const pendingInfo = await this.getClient().pendingTransactionInformation(txId);
            if (pendingInfo.round !== null && pendingInfo.round > 0) {
                //Got the completed Transaction
                console.log("Transaction " + pendingInfo.tx + " confirmed in round " + pendingInfo.round);
                break;
            }
            lastRound++;
            await this.getClient().statusAfterBlock(lastRound);
        }
    };

    async createAsset(wallet, assetName, unitName, supply, assetURL, managerAddress, reserveAddress, freezeAddress, clawbackAddress, decimals) {
        let cp = await this.getChangingParams();

        let note = new Uint8Array(Buffer.from("algodesk", "base64"));
        let addr = wallet.address;
        let defaultFrozen = false;

        if (decimals == undefined || decimals == null || decimals == "") {
            decimals = 0;
        }
        const txn = sdk.makeAssetCreateTxn(addr, cp.fee, cp.firstRound, cp.lastRound, note,
            cp.genHash, cp.genID, supply, decimals, defaultFrozen, managerAddress, reserveAddress, freezeAddress, clawbackAddress,
            unitName, assetName, assetURL);


        let rawSignedTxn = txn.signTxn(wallet.secretKey);

        let transaction = (await this.getClient().sendRawTransaction(rawSignedTxn, {'Content-Type': 'application/x-binary'}));
        return transaction;
    }

    async modifyAsset(wallet, assetId, managerAddress, reserveAddress, freezeAddress, clawbackAddress) {
        let cp = await this.getChangingParams();

        let note = new Uint8Array(Buffer.from("algodesk", "base64"));
        let addr = wallet.address;
        let txn;
        try {
            txn = sdk.makeAssetConfigTxn(addr, cp.fee, cp.firstRound, cp.lastRound, note,
                cp.genHash, cp.genID, assetId, managerAddress, reserveAddress, freezeAddress, clawbackAddress);
        }
        catch (e) {
            console.log(e);
        }

        let rawSignedTxn = txn.signTxn(wallet.secretKey);

        let transaction = (await this.getClient().sendRawTransaction(rawSignedTxn, {'Content-Type': 'application/x-binary'}));
        return transaction;
    }

    async pendingTransactionInformation(txId) {
        return await this.getClient().pendingTransactionInformation(txId);
    }

    async destroyAsset(wallet, assetId) {
        let cp = await this.getChangingParams();
        const addr = wallet.address;
        let note = new Uint8Array(Buffer.from("algodesk", "base64"));

        let txn = sdk.makeAssetDestroyTxn(addr, cp.fee,
            cp.firstRound, cp.lastRound, note, cp.genHash,
            cp.genID, assetId);
        let rawSignedTxn = txn.signTxn(wallet.secretKey);

        let transaction = (await this.getClient().sendRawTransaction(rawSignedTxn, {'Content-Type': 'application/x-binary'}));
        return transaction;
    }

    async freezeAsset(wallet, assetId, freezeAddress, freezeState) {
        let cp = await this.getChangingParams();
        const addr = wallet.address;
        let note = new Uint8Array(Buffer.from("algodesk", "base64"));
        if (!freezeState) {
            freezeState = false;
        }

        let txn = sdk.makeAssetFreezeTxn(addr, cp.fee,
            cp.firstRound, cp.lastRound, note, cp.genHash, cp.genID,
            assetId, freezeAddress, freezeState);

        let rawSignedTxn = txn.signTxn(wallet.secretKey);

        let transaction = (await this.getClient().sendRawTransaction(rawSignedTxn, {'Content-Type': 'application/x-binary'}));
        return transaction;
    }

    async sendAssets(wallet, assetId, recipient, amount) {
        let cp = await this.getChangingParams();

        let note = new Uint8Array(Buffer.from("algodesk", "base64"));
        let sender = wallet.address;
        const revocationTarget = undefined;
        const closeRemainderTo = undefined;

        const txn = sdk.makeAssetTransferTxn(sender, recipient,
            closeRemainderTo, revocationTarget,cp.fee, amount,
            cp.firstRound, cp.lastRound, note, cp.genHash, cp.genID, assetId);

        let rawSignedTxn = txn.signTxn(wallet.secretKey);

        let transaction = (await this.getClient().sendRawTransaction(rawSignedTxn, {'Content-Type': 'application/x-binary'}));
        return transaction;
    }

    async revokeAssets(wallet, assetId, revokeAddress, revokeReceiverAddress, revokeAmount) {
        let cp = await this.getChangingParams();

        let note = new Uint8Array(Buffer.from("algodesk", "base64"));
        let sender = wallet.address;
        const revocationTarget = revokeAddress;
        const closeRemainderTo = undefined;

        const txn = sdk.makeAssetTransferTxn(sender,
            revokeReceiverAddress, closeRemainderTo, revocationTarget,
            cp.fee, revokeAmount, cp.firstRound, cp.lastRound,
            note, cp.genHash, cp.genID, assetId);

        let rawSignedTxn = txn.signTxn(wallet.secretKey);

        let transaction = (await this.getClient().sendRawTransaction(rawSignedTxn, {'Content-Type': 'application/x-binary'}));
        return transaction;
    }

    isValidAddress(addr) {
        return sdk.isValidAddress(addr);
    }

    getNetworks() {
        return this.networks;
    }
}

export default new AlgoSdk();

Implement Account Login

Now that we have the service class, we will create the Login component, which looks like this.

EditorImages/2020/05/12 13:46/login.png

When the user clicks the “Get Started” button, this activates the following two steps:

  1. Derive wallet address and secret key from user provided mnemonic.
  2. Get Account details by address which includes balance and list of assets associated with that account.

import algoSdk from "../../App/services/algoSdk";
var keys = algoSdk.mnemonicToSecretKey(mnemonic);
algoSdk.getAccountInformation(keys.addr).then((payload) => {
    let walletDetails = {
        ...payload,
        secretKey: keys.sk,
        mnemonic
    };
    const assets = [];
    const {thisassettotal} = walletDetails;
    for (let key in thisassettotal) {
        assets.push({
            ...thisassettotal[key],
            id: key
        });        
    }
  console.log(assets);
}).catch(error => {
    console.log(error)
});

Now that we have the wallet address, secret key and assets associated with this account, we redirect the user to dashboard page.

Info

Before redirecting make sure, the wallet details, secret key and assets are saved in redux using reducers. You can learn about redux actions and reducers here.

Assets Dashboard

Now we are on the Dashboard Page. The default Component on this page is HOME. It has a “Create Asset” button and a list of assets associated with this account that we saved during wallet login.

The Home Component looks like this:

EditorImages/2020/05/12 13:46/home.png

Here, we display the Creator Address, Manager Address, Reserve Address, Freeze Address, and Clawback Address.

We have all these details in tge “asset” object we saved during wallet login.
You can learn more about Asset configuration here.

Create an Asset

Clicking on the “Create Asset” button will open a Modal Dialog (Material UI) reference.

The Modal Dialog renders the Create Asset Component, which looks like this:

EditorImages/2020/05/17 09:50/create.png

When the user clicks the “Create” button, the following 4 steps occur:

  1. Validate all the configuration properties.
  2. Push the create asset transaction to the blockchain.
  3. Wait for confirmation.
  4. Get transaction details on confirmation, close the Modal Dialog, and load the created asset.

import algoSdk from "../../App/services/algoSdk";

try {
    const {txId} = await algoSdk.createAsset(wallet, assetName, unitName, supply, assetUrl, managerAddress, reserveAddress, freezeAddress, clawbackAddress, decimals);
    await sdk.waitForConfirmation(txId);
    const txDetails = await algoSdk.pendingTransactionInformation(txId);
    const createdAsset = txDetails.txresults.createdasset;
    return txDetails;
}
catch (e) {
    console.log(e);
}

At this point, we have ID of the asset created in this transaction. Now we need to get details of the created asset.

import algoSdk from "../../App/services/algoSdk";

algoSdk.getAssetInformation(assetId).then((payload) => {
    const assetDetails = payload;
    console.log(assetDetails);
})
.catch(error => {
    console.log(error);
});

Now that we have the details of the newly created asset, add this asset to list of assets in the redux store and it instantly reflects in the UI.

So far we have learned how to create an asset and show list of assets associated with an account. Next we will see how to Modify, Destroy, Freeze and Unfreeze the assets.

Modify an Asset

Here, we create a context menu for each asset listed on the home page.

The context menu UI looks like this:

EditorImages/2020/05/17 09:53/menu.png

Clicking on each menu item will perform its corresponding action. For example, clicking on Modify should render the Modify Asset Component. The Modify Asset UI looks like this:

EditorImages/2020/05/17 09:50/modify.png

Notice that Asset Name, Unit Name, and Supply are disabled, because they cannot be modified after the asset has been created. Only the Manager Address, Reserve Address, Freeze Address and Unfreeze Address are configurable.

When the user clicks the “Modify: button, there are 3 steps involved:

  1. Push modify asset transaction to blockchain.
  2. Wait for confirmation.
  3. Get transaction details confirmation, close the Modal Dialog, and update the asset in the redux store.

import algoSdk from "../../App/services/algoSdk";

try {
    const {txId} = await sdk.modifyAsset(wallet, assetId, managerAddress, reserveAddress, freezeAddress, clawbackAddress);
    await sdk.waitForConfirmation(txId);
    const txDetails = await sdk.pendingTransactionInformation(txId);
    console.log(txDetails);
    return txDetails;
}
catch (e) {
    console.log(e);
}

Once the transaction is confirmed, load the asset details and refresh that asset with new details in the redux store.

import algoSdk from "../../App/services/algoSdk";

algoSdk.getAssetInformation(assetId).then((payload) => {
    const assetDetails = payload;
    console.log(assetDetails);
})
.catch(error => {
    console.log(error);
});

Similarly, you can implement Delete, Freeze, Unfreeze, Revoke and Transfer as the API’s are already available in the service. Then, the Asset Management Portal is complete.

What’s Next ?

  1. This is a completely serverless app, so every time the page is reloaded, the user has to enter their mnemonic which is quite annoying. At the same time, it is not a good idea to save a plain text mnemonic in the user’s browser, because anyone can see it using developer tools. One way to resolve this is to encrypt the mnemonic with a user provided password and save the encrypted mnemomic on the browsers local storage. The password is required whenever a transaction has to be made from his account. This makes the app more secure.
  2. Implement Save Asset Details on the browser’s indexDB using https://dexie.org/. So that we don’t have to load assets every time the user logs in.