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!