Solutions
No Results

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

Framework Integration: Exploring Express.js With the Algorand SDK

Overview

In this tutorial, we’ll explore how you can use Algorand through the js-algorand-sdk with Express.js. Express is a popular web framework to quickly build APIs using its myriad of HTTP utility methods and middleware options. For Algorand specifically, you’ll explore the following topics:

  • How to query transactions
  • How to query asset-related information
  • How to format responses to improve readability of Algorand data
  • How to query account-related information and apply filters
  • How to create accounts with the kmd client

Requirements

Make sure you have an active installation of the Algorand sandbox to follow this tutorial. Also, you need the latest Node and npm version installed on your machine. You can also follow this tutorial using a testnet node or the Purestake API service.

Installation

To access the code, clone the starter project from GitHub.

First, let’s make sure we can run the code. Don’t forget to install all dependencies by running npm install. When that’s done, try starting the project with npm start. You should see the following message pop up in your terminal output. Your API should run on your local machine on port 3000.

Example app listening at http://localhost:3000

Architecture

The Express app contains three main routes:
- localhost:3000/transactions
- localhost:3000/assets
- localhost:3000/accounts

They will form the basis for creating new endpoints. Therefore, the contents of the app.js file are very straightforward, with a welcome endpoint at the root of the API (/).

const express = require('express')
const app = express()
const port = 3000

const transactions = require('./routes/transactions')
const assets = require('./routes/assets')
const accounts = require('./routes/accounts')

app.get('/', (req, res) => {
  res.send('Hello Algorand!')
})

// routes
app.use('/transactions', transactions)
app.use('/assets', assets)
app.use('/accounts', accounts)

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

Let’s get started with a transactions endpoint.

Step 1: Querying Transactions

Let’s query for the ten latest transactions and display the formatted response via the following API endpoint: localhost:3000/transactions/. Before we explore the endpoint, make sure to send some transactions using goal and the sandbox. Make sure to replace the <to> and <from> inputs with the addresses generated by your sandbox instance.

./sandbox goal clerk send -a 1000000 -t <to> -f <from>

Here’s the code to retrieve transactions using the Algorand Indexer client exposed by the algosdk package. Don’t forget first to make a connection object. If you are using the Algorand sandbox, you can use the client configuration shown in the code snippet below.

var express = require("express");
const router = express.Router();

const algosdk = require("algosdk");
const indexer_token = "";
const indexer_server = "http://localhost";
const indexer_port = 8980;

// Instantiate the indexer client wrapper
let client = new algosdk.Indexer(indexer_token, indexer_server, indexer_port);

// get latest 10 transactions
router.get("/", async function (req, res) {
  let limit = 10;
  let transactionInfo = await client.searchForTransactions().limit(limit).do();

  const transactions = transactionInfo.transactions.map((tx) => ({
    id: tx.id,
    fee: tx.fee,
    confirmed: tx['confrimed-round'],
    from: tx.sender,
    to: tx['payment-transaction'].receiver,
    amount: tx['payment-transaction'].amount,
    algo: (tx['payment-transaction'].amount / 1000000)
  }));

  res.send(transactions);
});

module.exports = router;

To retrieve the latest ten transactions, we call the searchForTransactions function. Other than that, we can call additional functions such as a limit function. Via this function, we can tell the indexer we only want to retrieve the ten latest transactions. Make sure to call the do method at the end of your request to return a promise. Then we can await the result and format the response.

Note that we both return the amount in microalgos and in ALGO tokens by dividing the number by 1000000. Also, make sure to use the following syntax for object properties that contain a dash in their name:

// correct
tx['payment-transaction']

// doesn't work
tx.payment-transaction

Here’s the JSON response you get when calling localhost:3000/transactions/:

[
  {
    "id": "DYYG2PK3L673O6NWITUJDN54QEZ7RHAIFNNKUZHVCQ66CVNSKCKQ",
    "fee": 1000,
    "from": "HMRCETTEG5HEPBJRDQH4URGZJNJB4SF2VKAR5WF523VQ7ZUG423V4AEJTE",
    "to": "KI4DJG2OOFJGUERJGSWCYGFZWDNEU2KWTU56VRJHITP62PLJ5VYMBFDBFE",
    "amount": 20000000,
    "algo": 20
  },
  {
    "id": "LWK72WLSMWRV7FP7M5MOQVOARA5VSDGYWNLZAEVW7PYUQ2YMRDIA",
    "fee": 1000,
    "from": "HMRCETTEG5HEPBJRDQH4URGZJNJB4SF2VKAR5WF523VQ7ZUG423V4AEJTE",
    "to": "KI4DJG2OOFJGUERJGSWCYGFZWDNEU2KWTU56VRJHITP62PLJ5VYMBFDBFE",
    "amount": 1000,
    "algo": 0.001
  },
  {
    "id": "L6CREASO5FAH35S2WKK2JZUH4PFWH4GA3WDGLFGAXFADBFWXDCAQ",
    "fee": 1000,
    "from": "HMRCETTEG5HEPBJRDQH4URGZJNJB4SF2VKAR5WF523VQ7ZUG423V4AEJTE",
    "to": "IOHE7LY4HIZHMAU5JABUJMKZEIITKXZFKSXPHLCFDBWYZ437YERIHRRZY4",
    "amount": 1000000,
    "algo": 1
  }
]

Next, let’s explore the assets endpoint.

Step 2: Querying Assets

Next thing up is querying for assets by name. For that reason, let’s create an endpoint in the routes/assets.js file where we accept a :name parameter. The entire route for this endpoint is localhost:3000/assets/:name

To query assets, let’s create one. Make sure also to pass the --name flag to add a name to the asset. Without this flag, the goal command below will only set the unitname, which means you can’t query the asset by its name. Here’s the command to create a new asset called USDCA with a supply of 1000 tokens and zero decimals. Again, replace the <from> input placeholder with an address from your sandbox.

./sandbox goal asset create --creator <from> --total 1000 --unitname USDCA --name USDCA --asseturl https://usdc.com --decimals 0

When looking at the route implementation, we obviously need to get access to the name parameter via the req object. Further, we can call the searchForAssets function and pass a name via the name function. This function will return an array with all matching assets.

router.get("/:name", async function (req, res) {
  const assetSearchName = req.params.name;
  console.log("Searching for asset: ", assetSearchName)

  let assetInfo = await client
    .searchForAssets()
    .name(assetSearchName)
    .do();

  const assets = assetInfo.assets.map(asset => ({
      id: asset.index,
      decimals: asset.params.decimals,
      name: asset.params.name,
      total: asset.params.total,
      frozen: asset.params["default-frozen"]
  }))

  res.send(assets);
});

For instance, if you search for USD, you’ll find all assets that start with the prefix USD, including our USDCA token. You can try it yourself by calling http://localhost:3000/assets/USD. You can see the result here:

[
  {
    "id": 5,
    "decimals": 0,
    "name": "USDCA",
    "total": 1000,
    "frozen": false
  }
]

If you forgot to set a name for your asset and only provided a unitname, you can still query the asset with the function unit instead of name.

You can find all possible filters for the searchForAssets function in the js-algorand-sdk. This file lists all the possible functions you can use in conjunction with searchForAssets.

export default class SearchForAssets extends JSONRequest {
  // eslint-disable-next-line class-methods-use-this
  path() {
    return '/v2/assets';
  }

  // limit for filter, as int
  limit(limit: number) {
    this.query.limit = limit;
    return this;
  }

  // asset creator address for filter, as string
  creator(creator: string) {
    this.query.creator = creator;
    return this;
  }

  // asset name for filter, as string
  name(name: string) {
    this.query.name = name;
    return this;
  }

  // asset unit name for filter, as string
  unit(unit: string) {
    this.query.unit = unit;
    return this;
  }

  // asset ID for filter, as int
  index(index: number) {
    this.query['asset-id'] = index;
    return this;
  }

  // used for pagination
  nextToken(nextToken: string) {
    this.query.next = nextToken;
    return this;
  }

  // include all items including closed accounts, deleted applications, destroyed assets, opted-out asset holdings, and closed-out application localstates
  includeAll(value = true) {
    this.query['include-all'] = value;
    return this;
  }
}

The next section explains how to query for accounts.

Step 3.1: Querying Accounts

Ok, let’s implement an endpoint to gather information for an address such as its balance and assets. The accounts route will look like this: localhost:3000/accounts/:address.

The implemented logic uses the lookupAccountByID function to retrieve all details for an address. For the assets, we format them to add the asset id and the amount the account owns of it.

router.get("/:address", async function(req, res) {
    let acc = req.params.address
    let accountInfo = await client.lookupAccountByID(acc).do()

    const account = {
        address: req.params.address,
        amount: accountInfo.account.amount,
        algo: (accountInfo.account.amount / 1000000),
        assets: accountInfo.account.assets.map(asset => ({ id: asset['asset-id'], amount: asset.amount })),
        status: accountInfo.account.status
    }

    res.send(account)
});

Unfortunately, this function doesn’t return more information about the asset like its asset name.

{
  "address": "<address>",
  "amount": 1004936765526641,
  "algo": 1004936765.526641,
  "assets": [
    {
      "id": 5,
      "amount": 1000,
    }
  ],
  "status": "Offline"
}

For that reason, the below code shows how you can pass the extended flag to get more asset details in your response.

router.get("/:address", async function(req, res) {
    let acc = req.params.address
    let accountInfo = await client.lookupAccountByID(acc).do()

    const account = {
        address: req.params.address,
        amount: accountInfo.account.amount,
        algo: (accountInfo.account.amount / 1000000),
        assets: accountInfo.account.assets.map(asset => ({ id: asset['asset-id'], amount: asset.amount })),
        status: accountInfo.account.status
    }

    if (typeof req.query.extended !== 'undefined' && account.assets.length > 0) {
        const assetsDetails = await Promise.all(account.assets.map(asset => client.lookupAssetByID(asset.id).do()))

        account.assets.map(asset => {
            const assetDetail = assetsDetails.find(assetDetail => asset.id === assetDetail.asset.index)

            asset.decimals = assetDetail.asset.params.decimals
            asset.name = assetDetail.asset.params.name || ''
            asset['unit-name'] = assetDetail.asset.params['unit-name'] || ''
            asset.url = assetDetail.asset.params.url
            return asset
        })
    }

    res.send(account)
});

Now, try calling the following endpoint with a valid address: localhost:3000/accounts/<address>?extended. You’ll get the following response:

{
  "address": "<address>",
  "amount": 1004936765526641,
  "algo": 1004936765.526641,
  "assets": [
    {
      "id": 5,
      "amount": 1000,
      "decimals": 0,
      "name": "USDCA",
      "unit-name": "USDCA",
      "url": "https://usdc.com"
    }
  ],
  "status": "Offline"
}

Step 3.2: Querying For Transactions Per Account

We can already query for basic account information. Let’s add an endpoint to query for all transactions for a certain address. The endpoint looks like this: localhost:3000/accounts/:address/transactions.

router.get("/:address/transactions", async function(req, res) {
    let acc = req.params.address
    let accountInfo = await client.lookupAccountTransactions(acc).do()

    const transactions = accountInfo.transactions.map(tx => {
        const transaction = {
            id: tx.id,
            fee: tx.fee,
            note: tx.note,
            from: tx.sender,
            type: tx['tx-type']
        }

        if (tx['tx-type'] === 'pay') {
            transaction.receiver = tx['payment-transaction'].receiver
            transaction.amount = tx['payment-transaction'].amount
        }

        return transaction
    })

    res.send(transactions)
});

Depending on the transaction type, we add extra information for pay (payment) type transactions. To query all transactions, you can use the lookupAccountTransactions function.

Step 4: Creating New Wallets and Accounts

In this last step, I want to show you how to create a new wallet and derive accounts using the js-algorand-sdk and kmd client.

First of all, you’ll have to create a new connection object for the kmd client. Make sure to pass the authentication token. For the sandbox, the below configuration works out of the box.

const kmdtoken = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const kmdserver = 'http://localhost';
const kmdport = 4002;

const kmdclient = new algosdk.Kmd(kmdtoken, kmdserver, kmdport);

Next, let’s use the kmdClient for our wallet creation endpoint at localhost:3000/accounts/create.

router.get("/create", async function (req, res) {
    console.log('Creating new wallet')
    let walletid = (await kmdclient.createWallet("MyTestWallet", "testpassword", "", "sqlite")).wallet.id;
    console.log("Created wallet:", walletid);

    let wallethandle = (await kmdclient.initWalletHandle(walletid, "testpassword")).wallet_handle_token;
    console.log("Got wallet handle:", wallethandle);

    let address1 = (await kmdclient.generateKey(wallethandle)).address;
    console.log("Created new account:", address1);

  res.send('account')
});

Note that we have hardcoded the wallet name and password as it’s not a best practice to share sensitive information like a wallet password via an API request. It’s more to show you how you can generate new wallets in your backend.

Using the createWallet function, you can create a new wallet by passing a name, password, and driver. In this case, the storage driver is sqlite. You don’t have to worry about that. We can further derive a wallethandle that can be used to generate new keys and retrieve addresses from them.

You’ll get a result similar to this in your CLI as we are printing the information.

Created wallet: 583a24113ffaac238098fdd0b0af7bce
Got wallet handle: 2f102532da73f7ff.e2ede299fc54f656bcfc1178d026bf72704a2debaa5e0ddaf71fb3a5e87b68af
Created new account: FFGCHJUQV5SKLYYWTL7OV66C234CH3QERYI33FZTPVWJU7LOFBUC4YMERI

That’s it!

August 26, 2021