Coder Social home page Coder Social logo

algorand-express's Introduction

Framework Integration: Exploring Express.js With the Algorand SDK

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

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.

Installation

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

Architecture

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.

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

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
  }
]

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

Step 5: Creating a Faucet to Fund Accounts

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:

Postman request sample

That's it!

Conclusion

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

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.