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
- How to create accounts with the
kmd
client - How to create a faucet to fund accounts
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 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.
Example app listening at http://localhost:3000
The Express app contains three main routes:
/transactions
/assets
/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 bodyParser = require("body-parser"); // Needed for parsing POST requests' body
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
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.
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 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
}
]
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"
}
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.
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
Let's look at the code at routes/transactions.js
to handle the incoming POST request to fund a new account. We've purposefully chosen for a POST request to show you how you can access parameters from the request. Here, we expect users to send a transaction to http://localhost:3000/transactions/faucet
. The user should also send a receiver
address of the account we want to fund with that request.
Firstly, we are importing the transactionsHelper
which contains the waitForConfirmation
function the Algorand documentation recommends using to check if a transaction has been confirmed. To keep the code clean, we've moved this code to a helper file.
Further, we find the connection details for the Algod client because writing to the blockchain requires the use of the Algod client. We'll also use the Algod client to query the transaction parameters for building our payment transaction.
After creating the algodClient
connection object, you'll find a hard-coded passphrase for the faucet. As a best practice, we recommend using dotenv to manage secrets securely. By default, this faucet sends 1234 microAlgos to the receiver
address. To retrieve the passphrase of a well-funded account with the Algorand sandbox, you can use goal
to export one of the automatically created accounts for your sandbox. Here's how you can do this:
./sandbox goal account export -a <address>
The passphrase is used to retrieve the address and secret key for the faucet account to further sign the payment transaction.
const transactionsHelper = require("../helpers/transactions")
const algodToken = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const algodServer = "http://localhost";
const algodPort = 4001;
let algodClient = new algosdk.Algodv2(algodToken, algodServer, algodPort);
const FAUCET_PASSPHRASE = "boss enemy gift student resource spend garment regret master across gun culture bag sauce pride grace steak general school wheel kiwi cannon upon abstract aisle"
// faucet: send 1234 microALGO tokens to a specific account
router.post("/faucet", async function (req, res) {
const receiver = req.body.receiver
const faucetAccount = algosdk.mnemonicToSecretKey(FAUCET_PASSPHRASE);
let params = await algodClient.getTransactionParams().do();
params.flatFee = true;
params.fee = 1000;
let note = algosdk.encodeObj("Faucet");
let txn = algosdk.makePaymentTxnWithSuggestedParams(
faucetAccount.addr,
receiver,
1234,
undefined,
note,
params
);
let signedTxn = txn.signTxn(faucetAccount.sk);
let txId = txn.txID().toString();
console.log("Signed transaction with txID: %s", txId);
await algodClient.sendRawTransaction(signedTxn).do();
await transactionsHelper.waitForConfirmation(algodClient, txId, 5);
res.send({
status: 200,
id: txId,
from: faucetAccount.addr,
to: receiver,
amount: 1234
})
})
Lastly, we use the exposed waitForConfirmation
function to poll the status of the transaction. If everything is correct, we return an object will all information about the faucet transaction. You can try out the request using a tool like Postman. Make sure to add a body
to the POST request. Here's how the body can look like with its response:
That's it!
To conclude this tutorial, here's an overview of all possible endpoints you can query:
- GET localhost:3000/ --> Hello world route
- GET localhost:3000/transactions/ --> Get latest 10 transactions
- POST localhost:3000/transactions/faucet Body: { receiver } --> Get 1234 microAlgos from faucet account
- GET localhost:3000/accounts/create --> Create new account
- GET localhost:3000/accounts/:address --> Get account info
- GET localhost:3000/accounts/:address?extended --> Get extended account info including details on the assets
- GET localhost:3000/accounts/:address/transactions --> Get transactions for account
- GET localhost:3000/assets/:name --> Search assets by name