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 Thumbnail
Intermediate · 1 hour

Sending Rewards to ICO Investors Using Batch Transactions in Python

Imagine you are the owner of a new ASA (Algorand Standard Asset), and you are looking for investors for your project via an Initial Coin Offering.

Those investors would send a number of Algos to your wallet in exchange for a proportional amount of your own token (your ASA). Note that the investors have to opt in to your ASA in order to receive it.

To accomplish that, you will need to perform the following steps:

  • Find all payment transactions made to your wallet and group them by sender address.
  • Calculate the amount of your token that you will give to each address proportionally to their investment.
  • Send a transaction to each address with the calculated amount of your token.

All the analyses and examples will be done on the TestNet, but the same logic would apply to the MainNet as well.

To simulate a real scenario, we will create 4 different wallets:

  • Wallet 1: Investor #1
  • Wallet 2: Investor #2
  • Wallet 3: Investor #3
  • Wallet 4: Our Wallet

Note that the creation of the wallets is optional, since you can follow all the steps with just the examples provided. Also, this tutorial does not cover smart contracts or smart signatures.

Requirements

Background

It is not mandatory but it is useful to read the following tutorials:

Steps

1. Environment Setup

Install all dependencies in your environment:

pip install requests
pip install numpy
pip install pandas
pip install py-algorand-sdk

2. Set Variables

In this example, we will be looking at transactions to our wallet made from 2022-02-07 to 2022-02-14. Once we got the investors’ addresses, we will reward them with our token (65875461), corresponding to a testcoin on the TestNet. The total amount of our token to be rewarded will be of 20000 units. This amount must be defined in micros.

my_wallet = "6HNCL2A5RZJN5LLTBB76NUCRM5TRFGMTSPIXT4JTP3A2WXZZQZKYNLP4HI"
my_token_id = "65875461"  # ASA identifier
total_tokens_to_pay = 20_000_000_000  # in micros
txn_start = "2022-02-07"
txn_end = "2022-02-14"

my_wallet_pk = ""  # Necessary to sign transactions
purestake_token = ""  # Your PureStake API Key

It is advisable to save your credentials as environment variables. Never share them with anyone.

3. Get Transactions

We will instantiate the indexer and define a function to obtain all transactions in a time period, given an address. We will collect those transactions as a pandas DataFrame.

import pandas as pd
from algosdk.v2client import indexer

headers = {
   "X-API-Key": purestake_token,
}
myindexer = indexer.IndexerClient(
    indexer_token="",
    indexer_address="https://testnet-algorand.api.purestake.io/idx2",  
    headers=headers
)


def get_txn(address, start_time, end_time):

    response = myindexer.search_transactions_by_address(
        address=address,
        start_time=start_time,
        end_time=end_time
    )
    transactions = response["transactions"]
    txn_df = pd.DataFrame(transactions)
    return txn_df


# Get txn done in address
txn_df = get_txn(my_wallet, txn_start, txn_end)

The transactions DataFrame provides multiple pieces of information, but we are only interested in the amount of Algos that each sender has paid. Thus, all unused columns will be removed from the data. Note that the amount is defined within another column, so we should extract it from there.

def prepare_txn(txn_df):

    # Filter for payments
    txn_filtered = txn_df[txn_df["tx-type"] == "pay"]

    # Retrieve useful columns
    txn_df_reduced = txn_filtered[["payment-transaction", "sender"]]

    # Get amount info from transaction (amount is given in microAlgos)
    txn_df_reduced["amount"] = txn_df_reduced["payment-transaction"].apply(pd.Series)["amount"] / 1_000_000
    txn_df_reduced.drop(["payment-transaction"], axis=1, inplace=True)

    return txn_df_reduced

# Clean df and retrieve necessary info
txn_cleaned = prepare_txn(txn_df)

An investor can make multiple transactions to our wallet, so we need to group all the transactions by sender address and add up their amounts. Since we will reward them proportionally, we will calculate the amount of our token to be sent to each address.

def group_txn_by_user(txn, total_tokens_to_pay):

    # Group txn by address
    txn_grouped = txn.groupby(["sender"]).sum()

    # Calculate amount % by address
    txn_grouped["amount_per"] = txn_grouped["amount"] / txn_grouped["amount"].sum()

    # Calculate tokens to pay to each address
    txn_grouped["tokens_to_pay"] = (txn_grouped["amount_per"] * total_tokens_to_pay)
    txn_grouped.sort_values(by=["tokens_to_pay"], ascending=False, inplace=True)

    return txn_grouped

# Group paid amount by address and set earned rewards
txn_grouped = group_txn_by_user(txn_cleaned, total_tokens_to_pay)

4. Pay Rewards

We will instantiate an AlgodClient on the TestNet using the PureStake credentials.

from algosdk.v2client import algod

algod_address = "https://testnet-algorand.api.purestake.io/ps2"
headers = {"X-Api-key": purestake_token}
algod_client = algod.AlgodClient(algod_token=purestake_token, algod_address=algod_address, headers=headers)

And we will define a function to send transactions from our wallet to another one. It will be necessary to set the token identifier (the coin we want to send), and the amount. The private key of your wallet will also be required to sign the transactions. Remember: do not share it with anyone.

import json
import base64
from algosdk.future.transaction import AssetTransferTxn


def do_transaction(my_wallet, my_wallet_pk, dest_adress, amount, my_token_id):

    # build transaction with suggested params
    params = algod_client.suggested_params()
    note = "Rewards".encode()  # the note will be seen by the receiver in the txn

    # construct txn
    unsigned_txn = AssetTransferTxn(
        sender=my_wallet,
        sp=params,
        receiver=dest_adress,
        index=my_token_id,
        amt=amount,
        note=note
    )

    # sign transaction
    signed_txn = unsigned_txn.sign(my_wallet_pk)

    # submit transaction
    txid = algod_client.send_transaction(signed_txn)
    print("Signed transaction with txID: {}".format(txid))

    # wait for confirmation
    try:
        confirmed_txn = wait_for_confirmation(algod_client, txid)
    except Exception as err:
        print(err)
        return

    print("Transaction information: {}".format(
        json.dumps(confirmed_txn, indent=4)))
    print("Decoded note: {}".format(base64.b64decode(
        confirmed_txn["txn"]["txn"]["note"]).decode()))

The wait_for_confirmation function is an utility from Algorand Inc that checks the transaction to be included in the blockchain.

# Function from Algorand Inc. - utility for waiting on a transaction confirmation
def wait_for_confirmation(client, txid):
    last_round = client.status().get('last-round')
    txinfo = client.pending_transaction_info(txid)
    while not (txinfo.get('confirmed-round') and txinfo.get('confirmed-round') > 0):
        print('Waiting for confirmation')
        last_round += 1
        client.status_after_block(last_round)
        txinfo = client.pending_transaction_info(txid)
    print('Transaction confirmed in round', txinfo.get('confirmed-round'))
    return txinfo

And finally, we will create a function to iterate over the DataFrame of investors we created and will carry out a transaction for each one of them to send their rewards.

def pay_battery(df_txs):

    for index, row in df_txs.iterrows():
        # token value to pay must be int; otherwise, txn will fail
        tokens_to_pay = row["tokens_to_pay"].astype(int).item()
        print("Starting transaction of {} to {}".format(tokens_to_pay, index))
        do_transaction(my_wallet, my_wallet_pk, index, tokens_to_pay, my_token_id)

# Pay rewards
pay_battery(txn_grouped)

5. Run Everything

import json
import base64
from algosdk.v2client import algod, indexer
from algosdk.future.transaction import AssetTransferTxn
import pandas as pd

# Set Variables
my_wallet = "6HNCL2A5RZJN5LLTBB76NUCRM5TRFGMTSPIXT4JTP3A2WXZZQZKYNLP4HI"
my_token_id = "65875461"  # ASA identifier
total_tokens_to_pay = 20_000_000_000  # in micros
txn_start = "2022-02-07"
txn_end = "2022-02-14"

my_wallet_pk = ""  # Necessary to sign transactions
purestake_token = ""  # Your PureStake API Key

# Instantiate Clients
headers = {
   "X-API-Key": purestake_token,
}
myindexer = indexer.IndexerClient(
    indexer_token="",
    indexer_address="https://testnet-algorand.api.purestake.io/idx2",
    headers=headers
)

algod_address = "https://testnet-algorand.api.purestake.io/ps2"
headers = {"X-Api-key": purestake_token}
algod_client = algod.AlgodClient(algod_token=purestake_token, algod_address=algod_address, headers=headers)


# Function to get transactions
def get_txn(address, start_time, end_time):

    response = myindexer.search_transactions_by_address(
        address=address,
        start_time=start_time,
        end_time=end_time
    )
    transactions = response["transactions"]
    txn_df = pd.DataFrame(transactions)
    return txn_df


# Function to clean transactions and retrieve necessary info
def prepare_txn(txn_df):

    # Filter for payments
    txn_filtered = txn_df[txn_df["tx-type"] == "pay"]

    # Retrieve useful columns
    txn_df_reduced = txn_filtered[["payment-transaction", "sender"]]

    # Get amount info from transaction (amount is given in microAlgos)
    txn_df_reduced["amount"] = txn_df_reduced["payment-transaction"].apply(pd.Series)["amount"] / 1_000_000
    txn_df_reduced.drop(["payment-transaction"], axis=1, inplace=True)

    return txn_df_reduced


# Function to group transactions by sender and calculate tokens to pay to them
def group_txn_by_user(txn, total_tokens_to_pay):

    # Group txn by address
    txn_grouped = txn.groupby(["sender"]).sum()

    # Calculate amount % by address
    txn_grouped["amount_per"] = txn_grouped["amount"] / txn_grouped["amount"].sum()

    # Calculate tokens to pay to each address
    txn_grouped["tokens_to_pay"] = (txn_grouped["amount_per"] * total_tokens_to_pay)
    txn_grouped.sort_values(by=["tokens_to_pay"], ascending=False, inplace=True)

    return txn_grouped


# Functions to perform the transactions
def do_transaction(my_wallet, my_wallet_pk, dest_adress, amount, my_token_id):

    # build transaction with suggested params
    params = algod_client.suggested_params()
    note = "Rewards".encode()  # the note will be seen by the receiver in the txn

    # construct txn
    unsigned_txn = AssetTransferTxn(
        sender=my_wallet,
        sp=params,
        receiver=dest_adress,
        index=my_token_id,
        amt=amount,
        note=note
    )

    # sign transaction
    signed_txn = unsigned_txn.sign(my_wallet_pk)

    # submit transaction
    txid = algod_client.send_transaction(signed_txn)
    print("Signed transaction with txID: {}".format(txid))

    # wait for confirmation
    try:
        confirmed_txn = wait_for_confirmation(algod_client, txid)
    except Exception as err:
        print(err)
        return

    print("Transaction information: {}".format(
        json.dumps(confirmed_txn, indent=4)))
    print("Decoded note: {}".format(base64.b64decode(
        confirmed_txn["txn"]["txn"]["note"]).decode()))


def wait_for_confirmation(client, txid):
    last_round = client.status().get('last-round')
    txinfo = client.pending_transaction_info(txid)
    while not (txinfo.get('confirmed-round') and txinfo.get('confirmed-round') > 0):
        print('Waiting for confirmation')
        last_round += 1
        client.status_after_block(last_round)
        txinfo = client.pending_transaction_info(txid)
    print('Transaction confirmed in round', txinfo.get('confirmed-round'))
    return txinfo


def pay_battery(df_txs):

    for index, row in df_txs.iterrows():
        # token value to pay must be int; otherwise, txn will fail
        tokens_to_pay = row["tokens_to_pay"].astype(int).item()
        print("Starting transaction of {} to {}".format(tokens_to_pay, index))
        do_transaction(my_wallet, my_wallet_pk, index, tokens_to_pay, my_token_id)


# Run all the Pipeline
def pay_rewards():

    # Get txn done in address
    txn_df = get_txn(my_wallet, txn_start, txn_end)

    # Clean df and retrieve necessary info
    txn_cleaned = prepare_txn(txn_df)

    # Group paid amount by address and set earned rewards
    txn_grouped = group_txn_by_user(txn_cleaned, total_tokens_to_pay)

    # Pay rewards
    pay_battery(txn_grouped)


if __name__ == '__main__':
    pay_rewards()

6. Review Transactions

As we said, we created 4 wallets to reproduce a real scenario:

  • Wallet 1: Investor #1
    • Address: 5D2OWR6X5GQOGYR4YA5JYIL5DCDTBMYS2BSNDIKXLH4UHSEQVDE2EVVR6I
    • 2 transactions (2A + 4A)
  • Wallet 2: Investor #2
    • Address: D5VFPNJTNMJIF2JDMWOEHPEZMFP2TNXXD63WNGK7HFS7X3AFQNVKNOIN4A
    • 1 transaction (11A)
  • Wallet 3: Investor #3
    • Address: 3CMEBJQPHDLA7ZS3D3VYXOQ3WHCSSOEV6JTZNCUJI25SL3WJAFLLOPLPII
    • 2 transactions (1A + 2A)
  • Wallet 4: Our Wallet
    • Address: 6HNCL2A5RZJN5LLTBB76NUCRM5TRFGMTSPIXT4JTP3A2WXZZQZKYNLP4HI

The transactions can be found on AlgoExplorer searching by address.

EditorImages/2022/02/17 00:48/Screenshot_2022-02-17_at_01.16.37.png

So, once the ICO has finished, it is time to reward the investors according to their contribution. Remember that in order to receive the rewards, they must opt in to your ASA in their wallets first. We set 20000 units of our token to be distributed among them. The amount they should receive would be as follows:

  • Wallet 1: 6000 TESTCOIN
  • Wallet 2: 11000 TESTCOIN
  • Wallet 3: 3000 TESTCOIN

EditorImages/2022/02/17 00:56/Screenshot_2022-02-17_at_01.26.07.png

As we ordered the received contributions from higher to lower, the sender with the highest contribution will be the first to receive the TESTCOIN. Thus, we have already succeeded in rewarding our investors.

7. Try it yourself

In order to simulate different scenarios, it is advisable to create different accounts on the TestNet and play around with them. One of them will represent your own wallet with your tokens; the others will represent investors sending you transactions.

You can find more information on how to create multiple wallets and fund them with Algos in the Create an Account on TestNet with Python tutorial.

After the creation, you can send transactions from one wallet to another using the Algorand Wallet.

To simulate your own token, you can swap some of your Algos on the TestNet to any other coin. There are multiple coins created for testing purposes. The testcoin (ASA ID 65875461) we chose is one of them. You can use the Tinyman DEX for swapping.

And to conclude, remember that the code and resources provided are meant to operate everything on the TestNet, but the same logic would apply to run everything on the MainNet as well.

8. Code Repository

git clone https://github.com/marcfresquet/algo-utilities.git