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

Tutorial

Intermediate · 1 hour

Integrating ALGO and ASA transfers within your application

In this tutorial you will learn how to integrate your application with the Algorand network:

  • How to create an address pool for your customer’s deposits
  • How to watch these addresses for deposit events
  • How to transfer Algos or ASA and check for transaction confirmation
  • What’s the Algorand transaction fee
  • What’s minimal balance

Expect to find code snippets illustrating the core operations.

Demo

You can find a sample app demonstrating creation of deposit accounts and watching for deposits on GitHub: https://github.com/bakoushin/algorand-deposit-example

Demo App

Requirements

To efficiently follow this tutorial it is recommended to deploy the Algorand Sandbox in the private network mode (this is the default one). All the examples in this tutorial heavily rely on the Sandbox infrastructure.

The additional benefit of familiarizing with Sandbox is that it provides a convenient CLI tool for interacting with the Algorand network.

Examples use official Algorand JavaScript SDK. Since the SDK APIs may be subject to change over time, if you want to ensure the code in the examples keeps working, consider installing the very same version that was used at the time of creating this tutorial:

npm install [email protected]

JavaScript code works with Node.js 12+.

Background

This tutorial is intended for developers familiar with JavaScript and basic blockchain concepts (transactions, blocks, etc.)

It is preferable but not required to have basic familiarity with the Algorand network and SDKs. Algorand Documentation is a great source of that knowledge.

Additional reading:

Steps

1. Creating address pool

To receive deposits from our customers we may want to create a pool of addresses that they can use to actually transfer assets to our custody. By assigning an individual account to each of the customers, we could easily manage all incoming deposits.

Standalone accounts

Creating an individual address is as easy as writing a single line of code:

const algosdk = require('algosdk');

const { addr, sk } = algosdk.generateAccount();

sk is the secret key needed to make further transactions from this address.

That way we could generate as many addresses as we want. For instance, we could generate an address once a customer is registered in our service.

Getting a mnemonic out of the secret key is that easy:

console.log('Mnemonic:', algosdk.secretKeyToMnemonic(sk));

Example on GitHub: generateAccount.js

As well as getting the address and its secret key back from the mnemonic:

// The same demo credentials are used across all code examples.
// Avoid using them in production code.
const mnemonic = 'ready web harsh use core absorb position leisure call price canoe fee lemon drum pear grid woman circle olive angry health camera shed above endless';

const { addr, sk } = algosdk.mnemonicToSecretKey(mnemonic);

Note: secret keys as well as mnemonics must be stored securely.

Master-derived accounts

However, it may be not very practical to keep track of all the distinct keys for each account. Instead, we can use a single key for all generated accounts using a Key Management Daemon (Kmd). This service is not publicly available as a third-party API, so we have either to set up a full node or spin up a Sandbox. In this tutorial, we are using a Sandbox.

Sandbox

Now it’s time to download and start a Sandbox if you didn’t earlier.

By default, the Sandbox starts in private network mode. It is extremely convenient during development since it provides right out the box:

  • Algorand services such as Algod, Kmd and Indexer all of which we will be using in this tutorial.
  • 3 brand new accounts already topped with test Algos.
  • Your own private network ready for experiments. What happened in Sandbox, stays in Sandbox.

Please refer to the Sanbox Documentation to find the most relevant information on how to download and start it, and learn about endpoints it exposes as well.

If you have already deployed a Sandbox, it has to be redeployed in the private network mode.

To restart an existing Sandbox in the private network mode, execute the following commands in the terminal:

./sandbox clean # wipe out all previous Sandbox settings
./sandbox up    # start the Sandbox in the default mode (private network)

Another useful command for Sandbox is ./sandbox reset. It reverts everything within the Sandbox to its default values. While following this tutorial, you can use it to revert everything back and start over again, if something goes wrong.

Kmd

We will interact with a kmd endpoint from our code. This endpoint is normally exposed in the Sandbox by default.

Just in case to ensure that kmd is up, execute the following command in the terminal:

./sandbox goal kmd start

Now we can create a determninistic wallet using master derivation key mnemonic. The cool thing about using the master derivation key is that all the accounts can be restored too in the order of their creation. Let’s see an example:

const algosdk = require('algosdk');

// Standard Kmd credentials in the Sandbox
const token = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const server = 'http://localhost';
const port = '4002';

const kmdClient = new algosdk.Kmd(token, server, port);

(async () => {
  // The same demo credentials are used across all code examples.
  // Avoid using them in production code.
  const walletName = 'deposit_wallet';
  const walletPassword = 'Passw0rd!';
  const walletMnemonic = 'lunar peace photo despair entry sketch zone cook recall lab float deposit proud sniff danger forest aware reopen allow mass horror boil alone above voyage';

  // Restore master derivation key from the mnemonic
  const masterDerivationKey = await algosdk.mnemonicToMasterDerivationKey(walletMnemonic);

  // Restore wallet
  const { wallet } = await kmdClient.createWallet(
    walletName,
    walletPassword,
    masterDerivationKey
  );

  console.log('Wallet successfully restored');
  console.log('Wallet Id:', wallet.id);

  // Generate first 10 accounts
  console.log('First 10 accounts:');

  const { wallet_handle_token } = await kmdClient.initWalletHandle(
    wallet.id,
    walletPassword
  );

  for (let i = 0; i < 10; i++) {
    const { address } = await kmdClient.generateKey(wallet_handle_token);
    console.log(address);
  }
})().catch((error) => {
  console.error(error);
});

Example on GitHub: generateAccountsUsingKmd.js

You can reset the Sandbox and run the code above all over again. You will always get the same list of addresses.

Note: kmd stores everything on the disk, which may be a security concern to handle.

In some cases it would be convenient to generate a wallet by hand in order to store its credentials somewhere like environment settings. There is a CLI command for this:

./sandbox goal wallet new my_new_wallet

It will ask for a password for the new wallet and finally will print a mnemonic of a master derivation key of a newly created my_new_wallet.

Note: the master derivation key mnemonic and the wallet password must be stored securely.

2. Withdrawals and transfers

We may want to transfer assets from our account to the customer’s one (withdrawal) and between our own accounts as well.
To do that, we create a transaction object specifying sender and receiver addresses as well as an amount to transfer. Then we sign the transaction using the sender’s secret key and finally, we send the transaction to the Algorand network.

Transactions are the base building block of any blockchain network and Algorand is no exception. Transactions are used in many different scenarios affecting the state of the ledger besides just sending money. Later in this tutorial, we will see an example of opt-in for an ASA, which is also a transaction.

Let’s transfer 0.5 Algo from the default Sandbox account to one we’ve created in our deposit_wallet. We will use kmd again to figure out account addresses and sign the transaction. Also, note the helper function we are using to ensure that transaction is settled in the ledger. This example shows the basic gist of any transaction on Algorand.

const algosdk = require('algosdk');

// Standard Algod credentials in the Sandbox
const algodToken = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const algodServer = 'http://localhost';
const algodPort = '4001';

const algodClient = new algosdk.Algodv2(algodToken, algodServer, algodPort);

// Standard Kmd credentials in the Sandbox
const kmdToken = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const kmdServer = 'http://localhost';
const kmdPort = '4002';

const kmdClient = new algosdk.Kmd(kmdToken, kmdServer, kmdPort);

(async () => {
  // The same demo credentials are used across all code examples.
  // Avoid using them in production code.
  const depositWalletPassword = 'Passw0rd!';
  const defaultWalletPassword = ''; // no password for the default wallet in the Sandbox

  // FIND ACCOUNT ADDRESSES

  const { wallets } = await kmdClient.listWallets();

  // Get the first one from Sandbox default addresses
  const defaultWallet = wallets.find(({ name }) => name === 'unencrypted-default-wallet');
  const { wallet_handle_token: defaultWalletHandle } =
    await kmdClient.initWalletHandle(defaultWallet.id, defaultWalletPassword);
  const {
    addresses: [defaultAddress]
  } = await kmdClient.listKeys(defaultWalletHandle);

  // Get first address from our deposit_wallet
  const depositWallet = wallets.find(({ name }) => name === 'deposit_wallet');
  const { wallet_handle_token: depositWalletHandle } =
    await kmdClient.initWalletHandle(depositWallet.id, depositWalletPassword);
  const {
    addresses: [depositAddress]
  } = await kmdClient.listKeys(depositWalletHandle);

  // CREATE A TRANSACTION OBJECT

  const sender = defaultAddress;
  const receiver = depositAddress;

  // Transaction amount must be specified in microAlgos
  const amount = algosdk.algosToMicroalgos(0.5);

  // Get transaction params template
  const params = await algodClient.getTransactionParams().do();
  const closeRemainderTo = undefined; // not used since we don't want to close the account
  const note = undefined; // no additional notes

  // Create new transaction object
  const txo = algosdk.makePaymentTxnWithSuggestedParams(
    sender,
    receiver,
    amount,
    closeRemainderTo,
    note,
    params
  );

  // Sign the transaction
  const blob = await kmdClient.signTransaction(
    defaultWalletHandle,
    defaultWalletPassword,
    txo
  );

  // Send transaction to the Algorand network
  const { txId } = await algodClient.sendRawTransaction(blob).do();

  // Wait until the transaction is settled using a helper function (see below)
  await waitForConfirmation(algodClient, txId);

  const humanReadableAmount = algosdk.microalgosToAlgos(amount);
  console.log(
    `${humanReadableAmount} Algo transferred from ${sender} to ${receiver}`
  );
})().catch((error) => {
  console.error(error);
});

// HELPER FUNCTION

/**
 * Resolves when transaction is confirmed.
 *
 * @param {Algodv2} algodClient Instance of the Algodv2 client
 * @param {String} txId Transaction Id to watch on
 * @param {Number} [timeout=60000] Waiting timeout (default: 1 minute)
 * @return {Object} Transaction information
 */
async function waitForConfirmation(algodClient, txId, timeout = 60000) {
  let { 'last-round': lastRound } = await algodClient.status().do();
  while (timeout > 0) {
    const startTime = Date.now();
    // Get transaction details
    const txInfo = await algodClient.pendingTransactionInformation(txId).do();
    if (txInfo) {
      if (txInfo['confirmed-round']) {
        return txInfo;
      } else if (txInfo['pool-error'] && txInfo['pool-error'].length > 0) {
        throw new Error(txInfo['pool-error']);
      }
    }
    // Wait for the next round
    await algodClient.statusAfterBlock(++lastRound).do();
    timeout -= Date.now() - startTime;
  }
  throw new Error('Timeout exceeded');
}

Example on GitHub: transferAlgo.js

Transaction fee

The minimum transaction fee is 0.001 Algo (or 1000 microAlgo). That fee permits you to make a transaction up to 1 Kb in size. This is more than enough for a payment transaction. We do not have to specify a fee explicitly. In that case, the network will automatically use a minimal one.

We can verify the fee using fee property on our transaction object.

console.log('Fee:', txo.fee); // output: 1000

Minimum balance

Each account should keep the minimum balance which is 0.1 Algo (or 100,000 microAlgo) after any transaction is made. If an account opted in for an ASA, each opt-in increases the minimum balance by an additional 0.1 Algo (more on working with ASAs in the following section).

We cannot transfer tokens from an account if after the transaction its balance would be below 0.1 Algo. If we want to wipe out the whole account, there is a way: we can specify a receiver address in closeRemainderTo parameter. Then the whole amount would be transferred. The amount in that case could be specified as 0.

Moreover, we cannot transfer to any account, if after the transaction its balance would be below 0.1 Algo. To mitigate this, we may want to check the expected receiver balance after a transaction before starting it and warn the user.

const algosdk = require('algosdk');

const algodToken = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const algodServer = 'http://localhost';
const algodPort = '4001';

const algodClient = new algosdk.Algodv2(algodToken, algodServer, algodPort);

const MINIMAL_BALANCE = 100000;

(async () => {
  // The same credentials are used across all code examples.
  // Avoid using them in production code.
  const addr1 = 'A7B3YODYRUP24PWUT45JL4G7EQA72FHY4KVL3A7CI6EXGGVD5O7SM6MAIQ';
  const addr2 = 'S6YJQLGRUSCKG5PVFW77PE7SKTU6AELITMCVXPQBE7EMLG5224KNQ4WH2A';

  const transferAmount = algosdk.algosToMicroalgos(0.001);

  await checkExpectedBalance(addr1, transferAmount);
  await checkExpectedBalance(addr2, transferAmount);
})().catch((error) => {
  console.error(error);
});

async function checkExpectedBalance(address, transferAmount) {
  console.log(`Cheking ${address} balance:`);

  const amount = algosdk.microalgosToAlgos(transferAmount);

  // Current account balance
  const { amount: currentAmount } = await algodClient.accountInformation(address).do();

  const expectedBalance = currentAmount + transferAmount;

  if (expectedBalance < MINIMAL_BALANCE) {
    const minimalAmount = algosdk.microalgosToAlgos(MINIMAL_BALANCE - transferAmount);

    console.log(`Insufficient balance to receive ${amount} Algo`);
    console.log(`Minimal transfer amount is ${minimalAmount} Algo\n`);
  } else {
    console.log(`Balance is sufficient to receive ${amount} ALgo\n`);
  }
}

Example on GitHub: checkAccountBalance.js

3. Sending and receiving Algorand Standard Assets (ASA)

We may want to send and receive not only Algos but any other assets created on Algorand (Algorand Standard Assets, or ASA). This is where things are getting a bit tricky.

By default, any Algorand address is not eligible to receive any ASA, unless it is opted-in explicitly. To do that, we must create an opt-in transaction. This transaction implies transaction fees and some other requirements.

Assuming that our deposit accounts are created empty, in the case of ASA we have to ensure that an account we want to opt-in has a minimum balance needed to opt-in, including a transaction fee to be spent. At the time of the writing, the minimum amount is 0.201 Algo (or 201,000 microAlgo) for a single ASA. It is calculated as follows:

  • 0.1 Algo (100,000 microAlgo) is a default minimal balance to keep after a transaction. We discussed minimal balance in the previous section.
  • 0.1 Algo (100,000 microAlgo) is an additional minimum for each ASA we want to work with. Each next ASA adds another 0.1 Algo.
  • 0.001 Algo (1000 microAlgo) is a fee to spend for executing an opt-in transaction. Each next ASA adds another 0.001 Algo.

Thus if we want to opt-in for 2 ASAs simultaneously the minimum amount would be 0.302 Algo (or 302,000 microAlgo) and so on.

Note that transaction fees spend for the opt-in are at least 0.002 Algo (since we are making a transaction to top up an empty account), adding 0.001 Algo for each additional opt-in if needed.

The basic schema for an opt-in for a deposit account may be as follows:

  1. Top up an account with a minimum amount for the opt-in, including transaction fee
  2. Make an opt-in transaction

Creating a fictional ASA in the Sandbox

Since there is no ASA provided by default in the Sandbox, let’s first create a fictional one named TEST. Execute the following command in the terminal. Note it must be executed within the Sandbox directory.

./sandbox goal asset create --creator $(./sandbox goal account list | awk 'NR==1 {print $2}') --unitname TEST --total 1000 --decimals 0

It should return 1 as an ASA id. If it’s another number, please replace the ASSET_ID assignment in the following code.

Opt-in for receiving ASA

To opt-in for receiving an ASA, we will create 2 subsequent transactions as mentioned above.

With ASA we use a specific type of transaction. It is pretty similar to a regular transaction. The main difference is to specify the ASSET_ID of the ASA we are sending.

We have to create an empty transaction from and to the same address – the one we want to opt-in for an ASA.

const algosdk = require('algosdk');

// Replace the number with one obtained after ASA creation in the terminal.
// The command for ASA creation:
// ./sandbox goal asset create --creator $(./sandbox goal account list | awk 'NR==1 {print $2}') --unitname TEST --total 1000 --decimals 0
// Note it must be executed within the Sandbox directory.
// We are expecting that TEST asset id is 1.
const ASSET_ID = 1;

// Standard Algod credentials in the Sandbox
const algodToken = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const algodServer = 'http://localhost';
const algodPort = '4001';

const algodClient = new algosdk.Algodv2(algodToken, algodServer, algodPort);

// Standard Kmd credentials in the Sandbox
const kmdToken = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const kmdServer = 'http://localhost';
const kmdPort = '4002';

const kmdClient = new algosdk.Kmd(kmdToken, kmdServer, kmdPort);

(async () => {
  // The same demo credentials are used across all code examples.
  // Avoid using them in production code.
  const depositWalletPassword = 'Passw0rd!';
  const defaultWalletPassword = ''; // no password for the default wallet in the Sandbox

  // FIND ACCOUNT ADDRESSES

  const { wallets } = await kmdClient.listWallets();

  // Get the first one from Sandbox default addresses
  const defaultWallet = wallets.find(({ name }) => name === 'unencrypted-default-wallet');
  const { wallet_handle_token: defaultWalletHandle } = await kmdClient.initWalletHandle(defaultWallet.id, defaultWalletPassword);
  const { addresses: [defaultAddress] } = await kmdClient.listKeys(defaultWalletHandle);

  // Get first address from our deposit_wallet
  const depositWallet = wallets.find(({ name }) => name === 'deposit_wallet');
  const { wallet_handle_token: depositWalletHandle } = await kmdClient.initWalletHandle(depositWallet.id, depositWalletPassword);
  const { addresses: [depositAddress] } = await kmdClient.listKeys(depositWalletHandle);

  // MAKE AN OPT-IN

  // Create a transaction sending necessary amount to the account we want to opt-in (the top up transaction)
  const txo1 = await (async () => {
    const requiredAmount = 0.201; // Minimal balance + transaction fee (in Algos)
    const amount = algosdk.algosToMicroalgos(requiredAmount);
    const sender = defaultAddress;
    const receiver = depositAddress;
    const params = await algodClient.getTransactionParams().do();
    const closeRemainderTo = undefined; // not used since we don't want to close the account
    const note = undefined; // no additional notes

    const txo = algosdk.makePaymentTxnWithSuggestedParams(
      sender,
      receiver,
      amount,
      closeRemainderTo,
      note,
      params
    );

    return txo;
  })();

  // Create an opt-in transaction
  const txo2 = await (async () => {
    // Provide asset id we want to opt-in. We are expecting that it's 1.
    const assetId = ASSET_ID;
    const amount = 0;

    const params = await algodClient.getTransactionParams().do();
    const closeRemainderTo = undefined;
    const revocationTarget = undefined;
    const note = undefined;

    // Create opt-in transaction (note that sender and receiver addresses are the same)
    const txo = algosdk.makeAssetTransferTxnWithSuggestedParams(
      depositAddress,
      depositAddress,
      closeRemainderTo,
      revocationTarget,
      amount,
      note,
      assetId,
      params
    );

    return txo;
  })();

  algosdk.assignGroupID([txo1, txo2]);

  // Sign the top up transaction
  const blob1 = await kmdClient.signTransaction(
    defaultWalletHandle,
    defaultWalletPassword,
    txo1
  );

  // Sign the opt-in transaction
  const blob2 = await kmdClient.signTransaction(
    depositWalletHandle,
    depositWalletPassword,
    txo2
  );

  // Send both transactions as an atomic group
  const { txId } = await algodClient.sendRawTransaction([blob1, blob2]).do();
  await waitForConfirmation(algodClient, txId);

  console.log('Opt-in settled');
})().catch((error) => {
  console.error(error);
});

// HELPER FUNCTION

/**
 * Resolves when transaction is confirmed.
 *
 * @param {Algodv2} algodClient Instance of the Algodv2 client
 * @param {String} txId Transaction Id to watch on
 * @param {Number} [timeout=60000] Waiting timeout (default: 1 minute)
 * @return {Object} Transaction object
 */
async function waitForConfirmation(algodClient, txId, timeout = 60000) {
  let { 'last-round': lastRound } = await algodClient.status().do();
  while (timeout > 0) {
    const startTime = Date.now();
    // Get transaction details
    const txInfo = await algodClient.pendingTransactionInformation(txId).do();
    if (txInfo) {
      if (txInfo['confirmed-round']) {
        return txInfo;
      } else if (txInfo['pool-error'] && txInfo['pool-error'].length > 0) {
        throw new Error(txInfo['pool-error']);
      }
    }
    // Wait for the next round
    await algodClient.statusAfterBlock(++lastRound).do();
    timeout -= Date.now() - startTime;
  }
  throw new Error('Timeout exceeded');
}

Example on GitHub: optInASA.js

Sending ASA

Transaction for sending an ASA is almost identical to the opt-in one except for the amount, which in that case must be specified. Note that the receiver address must opt-in to receive ASA, otherwise the transaction will fail.

Remember to replace the ASSET_ID with the number obtained after ASA creation in the terminal (see above).

const algosdk = require('algosdk');

// Replace the number with one obtained after ASA creation in the terminal.
// We are expecting that it's 1.
const ASSET_ID = 1;

// Standard Algod credentials in the Sandbox
const algodToken = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const algodServer = 'http://localhost';
const algodPort = '4001';

const algodClient = new algosdk.Algodv2(algodToken, algodServer, algodPort);

// Standard Kmd credentials in the Sandbox
const kmdToken = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const kmdServer = 'http://localhost';
const kmdPort = '4002';

const kmdClient = new algosdk.Kmd(kmdToken, kmdServer, kmdPort);

(async () => {
  // The same demo credentials are used across all code examples.
  // Avoid using them in production code.
  const depositWalletPassword = 'Passw0rd!';
  const defaultWalletPassword = ''; // no password for the default wallet in the Sandbox

  // FIND ACCOUNT ADDRESSES

  const { wallets } = await kmdClient.listWallets();

  // Get the first one from Sandbox default addresses
  const defaultWallet = wallets.find(({ name }) => name === 'unencrypted-default-wallet');
  const { wallet_handle_token: defaultWalletHandle } = await kmdClient.initWalletHandle(
    defaultWallet.id,
    defaultWalletPassword
  );
  const { addresses: [defaultAddress] } = await kmdClient.listKeys(defaultWalletHandle);

  // Get first address from our deposit_wallet
  const depositWallet = wallets.find(({ name }) => name === 'deposit_wallet');
  const { wallet_handle_token: depositWalletHandle } = await kmdClient.initWalletHandle(
    depositWallet.id,
    depositWalletPassword
  );
  const { addresses: [depositAddress] } = await kmdClient.listKeys(depositWalletHandle);

  // TRANSFER AN ASA

  const assetId = ASSET_ID;
  const amount = 10; // 10 TEST

  const sender = defaultAddress;
  const receiver = depositAddress;

  const params = await algodClient.getTransactionParams().do();
  const closeRemainderTo = undefined;
  const revocationTarget = undefined;
  const note = undefined;

  // Create asset transfer transaction
  const txo = algosdk.makeAssetTransferTxnWithSuggestedParams(
    sender,
    receiver,
    closeRemainderTo,
    revocationTarget,
    amount,
    note,
    assetId,
    params
  );

  // Sign the transaction
  const blob = await kmdClient.signTransaction(
    defaultWalletHandle,
    defaultWalletPassword,
    txo
  );

  // Send the transactions
  const { txId } = await algodClient.sendRawTransaction(blob).do();
  await waitForConfirmation(algodClient, txId);

  console.log(amount, 'TEST sucessfully transferred');
})().catch((error) => {
  console.error(error);
});

// HELPER FUNCTION

/**
 * Resolves when transaction is confirmed.
 *
 * @param {Algodv2} algodClient Instance of the Algodv2 client
 * @param {String} txId Transaction Id to watch on
 * @param {Number} [timeout=60000] Waiting timeout (default: 1 minute)
 * @return {Object} Transaction object
 */
async function waitForConfirmation(algodClient, txId, timeout = 60000) {
  let { 'last-round': lastRound } = await algodClient.status().do();
  while (timeout > 0) {
    const startTime = Date.now();
    // Get transaction details
    const txInfo = await algodClient.pendingTransactionInformation(txId).do();
    if (txInfo) {
      if (txInfo['confirmed-round']) {
        return txInfo;
      } else if (txInfo['pool-error'] && txInfo['pool-error'].length > 0) {
        throw new Error(txInfo['pool-error']);
      }
    }
    // Wait for the next round
    await algodClient.statusAfterBlock(++lastRound).do();
    timeout -= Date.now() - startTime;
  }
  throw new Error('Timeout exceeded');
}

Example on GitHub: transferASA.js

3. Watching for deposits

As soon as we have the address pool deployed, we want to be notified when customers actually perform their deposits. We will use the Indexer service, which keeps track of all transactions in the Algorand network.

Since we want to process each deposit as soon as possible, we will regularly query Indexer for new transactions. As usual, we will rely on the Sandbox, which has Indexer enabled. In this example, we will submit a request every 1 second.
We will create an event emitter that will sniff all new transactions and will emit an event once it encounters an Algo or ASA deposit to one of the addresses we provided. We could implement that as a custom class extending Node.js standard EventEmitter API:

const EventEmitter = require('events');

class DepositWatcher extends EventEmitter {
  /**
   * @param {Indexer} indexer Instance of the Indexer client
   * @param {Array<string>} addresses Addresses to watch
   * @param {Number} [interval=1000] Query interval (default: 1 second)
   */
  constructor(indexer, addresses, interval = 1000) {
    super();

    this.indexer = indexer;
    this.addresses = new Set(addresses);
    this.interval = interval;

    this.seenRounds = new Map();
    this.lastRound = null;
    this.startTime = Date.now();

    this.processTransactions();
  }

  async processTransactions() {
    try {
      // We want to get all transactions since last round we've seen
      // or since current time if we don't know current round number yet.
      let query = this.indexer.searchForTransactions();
      if (this.lastRound) {
        query = query.minRound(this.lastRound);
      } else {
        query = query.afterTime(new Date().toISOString());
      }
      const queryResult = await query.do();

      // Types of transactions we want to process:
      // `pay` – Algo transfer
      // `axfer` – ASA transfer
      const txTypes = new Set(['pay', 'axfer']);

      for (const tx of queryResult.transactions) {
        const {
          id,
          sender,
          'tx-type': txType,
          'confirmed-round': confirmedRound,
          'round-time': roundTime
        } = tx;

        // Skip transactions happened before start of the watcher
        if (roundTime * 1000 < this.startTime) continue;

        // Skip transactions types we are not interested in
        if (!txTypes.has(txType)) continue;

        const { amount, receiver } =
          tx['payment-transaction'] || tx['asset-transfer-transaction'];

        // Process only deposits to our addresses
        if (!this.addresses.has(receiver)) continue;

        // Ensure skipping already processed transactions
        const hasBeenSeen =
          this.seenRounds.has(confirmedRound) &&
          this.seenRounds.get(confirmedRound).has(id);
        if (hasBeenSeen) continue;

        // Remember transaction round and id
        if (!this.seenRounds.has(confirmedRound)) {
          this.seenRounds.set(confirmedRound, new Set());
        }
        this.seenRounds.get(confirmedRound).add(id);

        // Finally, emit deposit event
        if (txType === 'axfer') {
          // ASA transfer
          const assetID = tx['asset-transfer-transaction']['asset-id'];
          this.emit('deposit_asa', { id, receiver, sender, assetID, amount });
        } else {
          // Algo transfer
          this.emit('deposit_algo', { id, receiver, sender, amount });
        }
      }

      // Clear seen transactions from previous rounds
      const currentRound = queryResult['current-round'];
      if (currentRound !== this.lastRound) {
        for (const key of this.seenRounds.keys()) {
          if (key < currentRound) this.seenRounds.delete(key);
        }
      }

      // Save round number to limit further queries
      this.lastRound = currentRound;
    } catch (error) {
      console.error(error);
    }

    // Repeat on given interval
    setTimeout(() => this.processTransactions(), this.interval);
  }
}

module.exports = DepositWatcher;

Example on GitHub: DepositWatcher.js

Note that the result of each transaction query contains at most 1000 records. For production-scale implementation, we may want to check the length of the result and fetch additional pages if necessary.

Now we can initialize an Indexer client, our brand new Deposit watcher and subscribe to deposit_algo and deposit_asa events. We are assuming that DepositWatcher is stored in a separate file DepositWatcher.js.

We will also use kmd to fetch the list of addresses stored in our deposit_wallet.

const algosdk = require('algosdk');
const DepositWatcher = require('./DepositWatcher');

// Standard Indexer credentials in the Sandbox
const indexerToken = ''; // no token in the Sandbox
const indexerServer = 'http://localhost';
const indexerPort = 8980;

const indexerClient = new algosdk.Indexer(indexerToken, indexerServer, indexerPort);

// Standard Kmd credentials in the Sandbox
const kmdToken = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const kmdServer = 'http://localhost';
const kmdPort = '4002';

const kmdClient = new algosdk.Kmd(kmdToken, kmdServer, kmdPort);

(async () => {
  // The same demo credentials are used across all code examples.
  // Avoid using them in production code.
  const depositWalletPassword = 'Passw0rd!';

  // Get addresses from deposit_wallet
  const { wallets } = await kmdClient.listWallets();
  const depositWallet = wallets.find(({ name }) => name === 'deposit_wallet');
  const { wallet_handle_token } = await kmdClient.initWalletHandle(depositWallet.id, depositWalletPassword);
  const { addresses } = await kmdClient.listKeys(wallet_handle_token);

  // Init DepositWatcher 
  const depositWatcher = new DepositWatcher(indexerClient, addresses);

  console.log('Waiting for deposits...');

  // Subscribe to deposit envents
  depositWatcher.on('deposit_algo', (txInfo) => {
    console.log('Algo Deposit: ', txInfo);
  });

  depositWatcher.on('deposit_asa', (txInfo) => {
    console.log('ASA Deposit: ', txInfo);
  });

})().catch((error) => {
  console.error(error);
});

Example on GitHub: watchDeposits.js

To make a deposit to one of our deposit accounts we can run the previously discussed transferAlgo.js and transferASA.js snippets.

Watching for deposits

Another useful way to make a deposit is to execute the following commands in the terminal. Note that the commands must be executed within the Sandbox directory.

Algo deposit:

./sandbox goal clerk send --from $(./sandbox goal account list | awk 'NR==1 {print $2}') --to A7B3YODYRUP24PWUT45JL4G7EQA72FHY4KVL3A7CI6EXGGVD5O7SM6MAIQ --a
mount 12345689

ASA deposit. Remember to replace the assetid with the number obtained after ASA creation in the terminal (see above). We are assuming it’s 1:

./sandbox goal asset send --from $(./sandbox goal account list | awk 'NR==1 {print $2}') --to A7B3YODYRUP24PWUT45JL4G7EQA72FHY4KVL3A7CI6EXGGVD5O7SM6MAIQ --assetid 1 --amount 123

You can find a sample app able to create deposit addresses and watch for deposits on GitHub: https://github.com/bakoushin/algorand-deposit-example

5. Additional resources

GitHub repositories:

Learn more:

August 18, 2021