Writing and reading to the Algorand blockchain via Telegram bot: live deployment

In my last article, I demonstrated how you can create a telegram bot, connect to the Algorand blockchain and run a few functions that reads from the chain (Algorand testnet) i.e create an account, check account balance and a few others. Today, we will try to experiment something more complex, which is simply writing and reading to the chain but in a more complex pattern than merely sending queries. We will deploy the application using Google App engine and anyone from any part of the world (with internet connection) can interact with it. 

What you will learn

  • It is possible to implement more complex logic using this application development paradigm.
  • Create fast and secure decentralized application that runs on Algorand blockchain layer 1.
  • Use case for Algorand and Telegram.
  • Deploy your Dapp in minutes.

Requirements

Note: You may find it tough if you are new to Python development. You should consider taking a few tutorial(s) from the official Python website or visit YouTube for couple of them. You will understand this tutorial if you diligently and patiently follow the steps. More so, required are the following peripherals hence I advise that you get them ready before continuing:

  • A computer (Desktop or Laptop) with at least 2 GB RAM.
  • Somewhere you will need internet connection.
  • Prior knowledge of Python programming or related programming language.
  • Ability to use the command line interface (not necessarily be a Pro).
  • Python Application (Python 3 or later version. I used Python 3.9)
    • Comes with pip by default. However if you have issue with it, refer to the pip documentation.
  • Algorand Python development Kit.
    • Ensure you have python installed, then run –> pip3 install py-algorand-sdk from the CLI.
    • Note: You may get error if you run pip install py-algorand-sdk while you have python3 or PiP3 installed.
  • Telegram application.
  • Python-telegram-bot library/sdk
    • pip install python-telegram-bot
  • An editor. (I recommend using VSCode or Pycharm).
  • Version control – preferably Git.

Before moving the car, we want to ensure the conditions for moving it are fulfilled. Same applies to our application. Please refer here for setting up your bot. Now let’s get to the code part. We will add more functionalities such as: TokenSale. This allows any telegram user to create an account on Algorand testnet, fund it with Algo token and purchase our token.

  • We created an Algorand Standard Asset with ticker ‘DMT2’ for our project and offer some as IPO.
  • Only 6,000,000 DMT2 is available for sale.
  • Any telegram user can purchase DMT2 by simply interacting with the BOT.

Firstly, a user should have an Algorand account or create one from the bot (note at testnet account). He funds it with test Algo from the dispenser and try to purchase some DMT2. Our program automatically subscribes the user to the token if their address is new to the contract by running the optIn() function, and finally forward requested asset amount to designated address provided all conditions are met. For clear understanding, I have used a crude pattern to write these instructions – reason for the plenty code. However, there is a smarter way of writing it to make it more robust(using python-like Algorand smart contract language – Pyteal). This we will explore in the coming tutorial. For now, let’s get to work.


Steps:

  • Program structure

Here is my file structure. You could a different structure from mine, however it works for you is fine. Create each of this file in your project directory. Although you could have all of the code python extended files in one place but for simplicity and fast debugging, it is advisable to have them in modules. If you wonder how I come by other non-py files, I had only:

  • Created a project algotelexam with root directory in algoTelExample(repo).
  • Made for a version control on Github with a new repository initialized with READme file and gitignore files.
  • Cloned the repo –> git clone <repo link here>
  • cd into the project directory
  • touch .env –> creates a .env file.
    • Install dotenv lib. Run: pip3 install python-dotenv
  • Run pip install -r requirements.txt from the git bash cli.
    • This installs all the requirements needed for the project.
4098df1de8587b1816d3dae823acda3131c22cab38023343fd5ae9f02414aca5.png

  • Codes

Let’s reveal the codes in each file. I will walk you through a few of them due to significant changes to the code from the last post.

  • client.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Get all required modules
import os
from algosdk.v2client import algod
from telegram import ReplyKeyboardMarkup
from dotenv import load_dotenv
from getInput import reply_keyboard_category

load_dotenv()
ALGODTOKEN = os.getenv('ALGODTOKEN')

"""Keyboard/menu parameters : passed to the CommandHandler object"""
markup = ReplyKeyboardMarkup(reply_keyboard_category, one_time_keyboard=True)

def connect(update, context):
    """
    Connect to an algorand node
    :param update:
    :param context:
    :return:
    """
    url = os.getenv('URL')  # Serve the endpoint to client node (https://purestake.com)
    algod_token = ALGODTOKEN   # Your personal token (https://purestake.com)
    headers = {"X-API-Key": algod_token}
    try:
        return algod.AlgodClient(algod_token, url, headers)
    except Exception as e:
        update.message.reply_text("Something went wrong.\nCould not connect to a node at this time.")

connect()establishes a connection to the Algorand network (in this case, Testnet).  Three things are important in this function thus:

  • algod_token
    • An access token since we are making connection via a third party service such as Purestake.
  • url
    • Server endpoint as the path (in this case, I am using querying the V2 client.
  • headers
    • Returns an object with a key-value telling the client we are requesting access via a registered vendor or node. 
    • Note that, this must be specified.
    • Finally we feed the arguments to the class AlgodClient.

  • generateAccount.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from algosdk import account, mnemonic
from telegram.inline.inlinekeyboardbutton import InlineKeyboardButton
from telegram.inline.inlinekeyboardmarkup import InlineKeyboardMarkup
# from telegram.replykeyboardmarkup import ReplyKeyboardMarkup
from client import connect
from getInput import *
import time

algod_client = connect(None, None)


def create_account(update, context):
    """
    Create new public/private key pair
    Returns the result of generating an account to user:
    :param update:
    :param context:
    :return: 1). An Algorand address, 2). A mnemonic seed phrase
    """
    update.message.reply_text("Hang on while I get you an account ...")
    time.sleep(1)
    try:
        sk, pk = account.generate_account()
        if not (pk and sk is None):
            update.message.reply_text("Account creation success.\nYour address:  {}\n"
                                      "Private Key:  {}\n\n"
                                      "Keep your mnemonic phrase from prying eyes.\n"
                                      "I do not hold or manage your keys."
                                      "".format(pk, sk)
                                      )
            context.user_data['default_pk'] = pk
        else:
            update.message.reply_text('Account creation error\n\n.')
            # return ConversationHandler.END
        update.message.reply_text('To test if your address works fine, copy your address, and visit:\n ')
        keyboard = [[InlineKeyboardButton(
            "DISPENSER", 'https://bank.testnet.algorand.network/', callback_data='1')]]

        reply_markup = InlineKeyboardMarkup(keyboard)

        update.message.reply_text('the dispenser to get some Algos\nSession ended.'
                                  'Click /start to begin.', reply_markup=reply_markup)
    except Exception as e:
        return e
    return STARTING


# Generate mnemonic words - 25 Algorand-type seed phrase
def get_mnemonics_from_sk(update, context):
    """
    Takes in private key and converts to mnemonics
    :param context:
    :param update:
    :return: 25 mnemonic words
    # """
    if 'Private_key' in context.user_data:
        sk = context.user_data['Private_key']
        phrase = mnemonic.from_private_key(str(sk))
        update.message.reply_text(
            "Your Mnemonics:\n {}\n\nKeep your mnemonic phrase from prying eyes.\n"
            "\nI do not hold or manage your keys.".format(phrase), reply_markup=markup_category
        )
        update.message.reply_text('\nSession ended.')
        del context.user_data['Private_key']
    else:
        update.message.reply_text("Key not found")
        return ConversationHandler.END
    return STARTING


def query_balance(update, context):
    """
    Check balance on an account's public key
    :param update:
    :param context:
    :return: Balance in account plus asset (s) balance
    """
    if 'Public_key' in context.user_data:
        pk = context.user_data['Public_key']
        update.message.reply_text("Getting the balance on this address ==>   {}.".format(pk))
        if len(pk) == 58:
            account_bal = algod_client.account_info(pk)
            bal = account_bal['amount']
            update.message.reply_text("Balance on your account: {}".format(bal), reply_markup=markup_category)
            for k in account_bal['assets']:
                update.message.reply_text(f"Asset balance: {k['amount']}, Asset ID: {k['asset-id']}\nClick /Menu"
                                          f" to go the main menu.")
            menuKeyboard(update, context)
        else:
            update.message.reply_text("Wrong address supplied.\nNo changes has been made.")
            return menuKeyboard(update, context)
    else:
        update.message.reply_text("Cannot find public key")
        menuKeyboard(update, context)
    return STARTING


def getPK(update, context):
    """
    Takes in 25 mnemonic and converts to private key
    :param context:
    :param update:
    :return: 25 mnemonic words
    # """
    if 'Mnemonic' in context.user_data:
        mn = context.user_data['Mnemonic']
        phrase = mnemonic.to_private_key(str(mn))
        update.message.reply_text(
            "Your Private Key:\n {}\n\nKeep your key from prying eyes.\n"
            "\n\nI do not hold or manage your keys.".format(phrase), reply_markup=markup_category
        )
        update.message.reply_text('\nSession ended.')
        del context.user_data['Mnemonic']
    else:
        update.message.reply_text("Cannot find Mnemonic.")
        return ConversationHandler.END
    return STARTING


def getAddress(update, context):
    if 'default_pk' in context.user_data:
        addr = context.user_data['default_pk']
        update.message.reply_text("Did you mean this? \n {}".format(addr), reply_markup=markup_category)
        # return ConversationHandler.END
    else:
        update.message.reply_text(
            "I don't have a record of your address\n"
            "Maybe you should create or supply one."
        )
        return ConversationHandler.END
    return STARTING

generateAccount file contain a couple of functions: 

  • create_account() –> Generates an account key pair, returns the keys to the user and the bot remembers only the public key.
  • get_mnemonic_from_sk() –> Requires an argument – params: private key to return a 25-word phrase to the user. Thereafter erases the private key.
    • Alert! – In production, you do want to take precaution having cognizance of users’ private information such as the private key and/or mnemonic. Here, AlgoSigner plays a vital role in web decentralized application development, perhaps Telegram is quite secure working with such data. You only want to ensure that the bot does not keep track of such data subsequent to the session where it was used. However, much of the onus and task of keeping the keys from prying eyes is shifted to the user as they should be more careful copying and pasting the keys. This may be viewed as one major challenge of this user-application interaction model.
  • query_balance() –> Takes the public key and return the balances of Algo token together with other ASAs the user already subscribed to.
  • getPK() –> User supplied mnemonic words and gets private key in return.
  • getAddress() –> Returns a store public key only if the user created an account from the bot.

  • optIn.py – contains code that opts in user if they have not opt in yet.
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from client import connect
import logging
from algosdk.future.transaction import AssetTransferTxn
from waitforconfirmation import wait_for_confirmation
import time

asset_id = 13251912


# First time account to opt in for DMT2 asset
def optin(update, context, recipient, sk):
    """
    Checks if user already optin for an ASA,
    subscribes users if condition is false.
    :param update: 
    :param context: 
    :param recipient: public key of subscriber
    :param sk: Signature of subscriber
    :return: true if success.
    """
    algod_client = connect(update, context)
    params = algod_client.suggested_params()
    # Check if recipient holding DMT2 asset prior to opt-in
    account_info_pk = algod_client.account_info(recipient)
    print(account_info_pk)
    holding = None
    # idx = 0
    for assetinfo in account_info_pk['assets']:
        scrutinized_asset = assetinfo['asset-id']
        # idx = idx + 1
        if asset_id == scrutinized_asset:
            holding = True
            msg = "This address has opted in for DMT2, ID {}".format(asset_id)
            logging.info("Message: {}".format(msg))
            logging.captureWarnings(True)
            break
    if not holding:
        # Use the AssetTransferTxn class to transfer assets and opt-in
        txn = AssetTransferTxn(sender=recipient,
                               sp=params,
                               receiver=recipient,
                               amt=0,
                               index=asset_id)
        # Sign the transaction
        # Firstly, convert mnemonics to private key.
        # For tutorial purpose, we will focus on using private key
        # sk = mnemonic.to_private_key(seed)
        sendTrxn = txn.sign(sk)

        # Submit transaction to the network
        txid = algod_client.send_transaction(sendTrxn)
        message = "Transaction was signed with: {}.".format(txid)
        wait = wait_for_confirmation(update, context, algod_client, txid)
        time.sleep(2)
        hasOptedIn = bool(wait is not None)
        if hasOptedIn:
            update.message.reply_text(f"Opt in success\n{message}")

        return hasOptedIn

  • status.py

It returns all information available to an account.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from waitforconfirmation import algod_client
from getInput import markup_category, inputcateg, STARTING


def account_status(update, context):
    """
    :param update: Telegram's default param
    :param context: Telegram's default param
    :param address: 32 bytes Algorand's compatible address
    :return: Address's full information
    """
    try:
        if 'Public-Key' in context.user_data:
            pk = context.user_data['Public-Key']
            status = algod_client.account_info(pk)
            for key, value in status.items():
                update.message.reply_text("{} : {}".format(key, value), reply_markup=markup_category)
        return STARTING
    except Exception as e:
        update.message.reply_text("Something went wrong.\nProbably I cannot find any key.\n"
                                  "Re /start and create an account or supply your public key "
                                  "if you have one.")

  • waitforconfirmation.py

Checks for confirmed transaction.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from client import connect

algod_client = connect(None, None)


def wait_for_confirmation(update, context, client, txid):
    """
    Utility function to wait until the transaction is
    confirmed.
    :param update: 
    :param context: 
    :param client: Connection info i.e request obj 
    :param txid: Hash/acknowledged receipt of the current broadcasted transaction
    :return: pending transaction.
    """
    last_round = algod_client.status().get('last-round')
    txinfo = client.pending_transaction_info(txid)
    update.message.reply_text("Waiting for confirmation...")
    while not (txinfo.get('confirmed-round')
               and txinfo.get('confirmed-round') > 0):
        last_round += 1
        txinfo = algod_client.pending_transaction_info(txid)
    return txinfo

  • purchase.py

This file contain functions that take input when the buy_token() is called. The flow is circular with an escape command that executes the invoked parent function i.e buy_token()

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import logging
from typing import Dict

from telegram import ReplyKeyboardMarkup, Update
from telegram.ext import CallbackContext
from buyToken import buy_token

# Enable logging
logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)

logger = logging.getLogger(__name__)

# A list of integers that controls the flow of execution
CHOOSING, TYPING_REPLY, TYPING_CHOICE = range(3)

# Parameters for accepting/supplying input.
reply_keyboard1 = [
    ['Public_key', 'Quantity'],
    ['Secret_Key', 'Note'],
    ['/Done'],
    ['/Main_menu'],
]
# Maps parameters to keyboard
markup2 = ReplyKeyboardMarkup(reply_keyboard1, one_time_keyboard=True)
user_d = {}


def facts_to_str(user_data: Dict[str, str]) -> str:
    """
    Takes the user_data object, strips context into a list
    :param user_data: 
    :return: Formated key-pair
    """
    arg = list()

    for key, value in user_data.items():
        arg.append(f'{key} - {value}')

    return "\n".join(arg).join(['\n', '\n'])


def args(update: Update, context: CallbackContext) -> int:
    """
    Entry point for taking arguments from user
    :param update: 
    :param context: 
    :return: The next line of action.
    """
    update.message.reply_text(
        "Enter the following information",
        reply_markup=markup2,
    )

    return CHOOSING


def regular_choice(update: Update, context: CallbackContext) -> int:
    """
    Compares supplied information to match what we expect.
    Passes it to DB if true.
    :param update: 
    :param context: 
    :return: The next line of action.
    """
    expected = ['Public_key', 'Quantity', 'Secret_Key', 'Note']
    text = update.message.text
    for b in expected:
        if text == b:
            user_d[b] = text
            update.message.reply_text(f'Enter {text.lower()}?')

    return TYPING_REPLY


def received_information(update: Update, context: CallbackContext) -> int:
    """
    Displays received arguments and passed it to temp_database
    :param update: 
    :param context: 
    :return: int (the next line of action)
    """
    text = update.message.text
    for a in user_d:
        category = user_d[a]
        if category == 'Public_Key' and len(text) == 58:
            assert len(text) == 58, update.message.reply_text("The address is invalid address")
            user_d[category] = text
        elif category == 'Quantity' and type(int(text) == int):
            user_d[category] = int(text)
        elif category == 'Secret_Key' and len(text) > 58:
            user_d[category] = text
        else:
            user_d[category] = text
        user_data = context.user_data
        user_data[category] = user_d[category]

    update.message.reply_text(
        "I got this from you:\n"
        f"{facts_to_str(user_d)}",
        reply_markup=markup2,
    )
    user_d.clear()

    return CHOOSING


def done(update: Update, context: CallbackContext):
    """
    Escape-exec func
    :param update: 
    :param context: 
    :return: Returns from buy_token()
    """
    buy_token(update, context)

  • buyToken.py

The emphasis and focus of this tutorial is on this file which contains function that executes the Tokensale contract.

Each time the buy_token() is called, the desired price and balance from the default account (otherwise contract account) are updated to their most recent state. Buyer is billed relative to quantity requested. We ensure that buyer has enough fund to cover the cost otherwise, the bot rejects the transaction. If all things being equal and conditions passes, our bot immediately transfer the amount of asset requested to the buyer.

Key Points

  • It is unsafe sending out assets with monetary value using this pattern. We use the DEFAULT_ACCOUNT2 variable as contract account from where the asset is sent. We could have an isolated contract account for such transaction that will be authorized by a logic instead of explicitly parsing authorized key from a usual account. This is where smart contract comes to play.

Please follow the documentation in the code. 

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from algosdk.future.transaction import transaction
from algosdk import mnemonic
from waitforconfirmation import wait_for_confirmation, algod_client
from algosdk.future.transaction import AssetTransferTxn
from client import markup
from optIn import optin
import logging
import os
from dotenv import load_dotenv
from telegram.ext import ConversationHandler
import time

load_dotenv()
price_progress = [1000, ]  # Tracks price change
current_price = price_progress[-1]
saleable = 6000000  # Total asset available for IPO.
total_buy = 0
assetBalance = 0
asset_id = 13251912

# Don't expose sensitive information.
to = os.getenv('DEFAULT2_ACCOUNT')
auth = os.getenv('DEFAULT2_MNEMONTIC')
rate = 30

# pull account information of supposedly contract account
accountInfo = algod_client.account_info(to)
successful = None


# Price updater.
# For every
def updatePrice(update, context):
    """
    Update the price after every 2000000 token is sold
        : price is increased by the
    half of the current price
    :param update:
    :param context:
    :return:
    """
    global rate
    global current_price

    if saleable >= 4000000:
        rate = rate
        current_price = current_price
        update.message.reply_text(
            "Current price per DMT2: {} MicroAlgo".format(current_price))
    elif 2000000 < saleable < 4000000:
        rate = 20
        newPrice = int(current_price + (current_price / 2))
        current_price = newPrice
        update.message.reply_text(
            "Current price per DMT2: {} MicroAlgo".format(newPrice))
    elif saleable <= 2000000:
        rate = 10
        newPrice = int(current_price + (current_price / 2))
        current_price = newPrice
        update.message.reply_text(
            "Current price per DMT2: {} MicroAlgo".format(newPrice))


def updateAssetBalance(update, context):
    """
    Continious update of asset balance in creator's account
    NB: We will need this for a check
    :param update:
    :param context:
    :return:
    """
    global assetBalance
    for i in accountInfo['assets']:
        if i['asset-id'] == asset_id:
            assetBalance = i['amount']
            break


def transfer(update, context, sender, receiver, amount):
    """
    Transfer a custom asset from default account A to account B (Any)
    :param update: Default telegram argument
    :param context: Same as update
    :param sender: Sender of this transaction
    :param receiver: The beneficiary of the asset in transit
    :param amount: Amount in custom asset other than ALGO
    :return: 
    """
    global saleable
    params = algod_client.suggested_params()
    params.fee = 1000
    params.flat_fee = True

    assert saleable >= amount, response_end(
        update, context, "Sales for the period is exhausted")

    txn = AssetTransferTxn(
        sender=sender,  # asset_manage_authorized,
        sp=params,
        receiver=receiver,
        amt=int(amount),
        index=asset_id)
    # Sign the transaction
    sk = mnemonic.to_private_key(auth)
    signedTrxn = txn.sign(sk)

    # Submit transaction to the network
    tx_id = algod_client.send_transaction(signedTrxn)
    message = "Successful! Transaction hash: {}.".format(tx_id)
    wait_for_confirmation(update, context, algod_client, tx_id)
    logging.info(
        "...##Asset Transfer... \nReceiving account: {}.\nMessage: {}\nOperation: {}\nTxn Hash: {}"
        .format(receiver, message, transfer.__name__, tx_id))

    update.message.reply_text(message)
    saleable -= amount
    # Erase the key soon as you're done with it.
    context.user_data.clear()
    return markup


def response_end(update, context, message):
    """
    Update the user with response with recourse to the executing function
    :param update:
    :param context:
    :param message: Message to forward to user --> str
    :return:
    """
    update.message.reply_text(message)
    return ConversationHandler.END


def buy_token(update, context):
    """
    Purchase an ASA: This function handles the whole computations, takes
    payment from the user and forward token remission to "transfer()"
    :param update:
    :param context:
    :return:
    """
    # Request to modify global variables. Peculiar to 
    # Python 3 or newer.
    global successful
    global current_price
    global price_progress
    global rate

    updatePrice(update, context)
    updateAssetBalance(update, context)

    update.message.reply_text("Broadcasting transaction...")
    
    # Extracts the arguments from the temp_DB
    user_data = context.user_data
    buyer = user_data["Public_key"]
    qty = user_data["Quantity"]
    sk = user_data["Secret_Key"]
    note = user_data["Note"].encode()
    
    # Firstly, opt in user if not already subscribe to DMT2 asset.
    optin(update, context, buyer, sk)
    time.sleep(3)
    max_buy = 500000  # Instant purchase pegged to 500,000 DMT2
    
    # If there is enough token to sell
    # Perform other necessary checks, then execute if all passed.
    if saleable > 0:
        try:
            params = algod_client.suggested_params()
            fee = params.fee = 1000
            flat_fee = params.flat_fee = True
            amountToPay = int(current_price * qty)
            alcBal = algod_client.account_info(buyer)['amount']
            assert alcBal > amountToPay, response_end(update, context, "Not enough balance.")
            assert qty <= max_buy, response_end(update, context, "Max amount per buy is restricted to 500000 DMT2.")
            assert qty >= 500, response_end(
                update, context, "Minimum buy is 500 DMT2.\n Session ended.")
            assert len(buyer) == 58, response_end(update, context,
                                                  "Incorrect address")

            raw_trxn = transaction.PaymentTxn(sender=buyer,
                                              fee=fee,
                                              first=params.first,
                                              last=params.last,
                                              gh=params.gh,
                                              receiver=to,
                                              amt=amountToPay,
                                              close_remainder_to=None,
                                              note=note,
                                              gen=params.gen,
                                              flat_fee=flat_fee,
                                              lease=None,
                                              rekey_to=None)

            # Sign the transaction
            signedTrxn = raw_trxn.sign(sk)
            update.message.reply_text("Just a second.....")
            # Submit transaction to the network
            tx_id = algod_client.send_transaction(signedTrxn)
            message = "Transaction hash: {}.".format(tx_id)
            wait = wait_for_confirmation(update, context, algod_client, tx_id)
            logging.info(
                "...##Asset Transfer... \nReceiving account: {}.\nMessage: {}\nOperation: {}\n"
                .format(buyer, message, buy_token.__name__))
            successful = bool(wait is not None)

            if successful:
                amountToSend = qty + (qty * (rate / 100))
                update.message.reply_text(
                    f"Payment success...\nTransferring {amountToSend} token to address... --> {buyer}"
                )
                transfer(update, context, to, buyer, amountToSend)
            else:
                response_end(update, context, "Transaction was not successful")

        except Exception as err:
            logging.info("Error encountered: ".format(err))
            update.message.reply_text("Unsuccessful")
    else:
        update.message.reply_text("Token unavailable at the moment")

  • main.py The main() is the heart of the bot which Keeps track of how program should run.
    Here you specify the token gotten from the BotFather, i.e the token for your bot. NB: Keep it secret.

    Updater class employs the telegram.ext.Dispatcher and provides a front-end to the bot for the users. So, you only need to focus on backend side.
    The ConversationHandler holds a conversation with a single  user by managing four collections of other handlers.
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from telegram.inline.inlinekeyboardbutton import InlineKeyboardButton
from telegram.inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from status import account_status
from buyToken import *
from getInput import *
from purchase import *

from generateAccount import (
    create_account,
    get_mnemonics_from_sk,
    query_balance,
    getPK,
    getAddress
)
import os
import logging
from dotenv import load_dotenv

from telegram.ext import (Updater, CommandHandler, Filters,
                          ConversationHandler, PicklePersistence,
                          CallbackContext, MessageHandler)

# Enable logging
logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    level=logging.INFO)

logger = logging.getLogger(__name__)

load_dotenv()
TOKEN = os.getenv('TOKEN')  # environ.get('BOT_TOKEN')  # Token from the bot father
updateAssetBalance(None, None)


def start(update, context: CallbackContext):
    """
    Gives direction for use
    Displays information about the bot and available commands
    :param update: 
    :param context: 
    :return: None
    """
    user = update.message.from_user
    reply = "Hi {}!\nI am ALGOMessenger.\n".format(user['first_name'])
    reply += (
        " Here are what you can do with this Algobot20.\n\n"
        "* /GetAlc - Create an account.\n"
        "* Get Mnemonic words from Private Key.\n"
        "* Check your account balance.\n"
        "* Buy DMT2 Token (Algorand Standard Asset.\n"
        "* Converts Mnemonic to Private key\n"
        "* Check account status.\n"
        "* Get your account public key (if you have created one with this bot.\n\n"
        "/About - about us.\n"
        "/Help - if you need help.\n"
        "/Cancel - ends a conversation.\n\n"
        "Navigate to /Main_menu to sub section."
    )
    update.message.reply_text(reply, reply_markup=markup)
    context.user_data.clear()


# Returns about us
def aboutUs(update, context):
    """
    Read more about Algorand and how you can build on Algorand
    :param update: 
    :param context: 
    :return: None
    """
    keyboard = [[
        InlineKeyboardButton("Website",
                             'https://algorand.com',
                             callback_data='1'),
        InlineKeyboardButton("Developer'site",
                             'https://developer.algorand.org',
                             callback_data='2')
    ],
        [
            InlineKeyboardButton("Community",
                                 'https://community.algorand.com',
                                 callback_data='3')
        ]]

    reply_markup = InlineKeyboardMarkup(keyboard)

    update.message.reply_text('Learn more about Algorand:',
                              reply_markup=reply_markup)


# Need help?
def help_command(update, context):
    """
    Gives direction for use
    :param update: 
    :param context: 
    :return: None
    """
    update.message.reply_text("Use /start to test this bot.")


def cancel(update, context):
    """
    Terminates a session and keeps the bot unengaged
    :param update: 
    :param context: 
    :return: int --> Ends the session
    """
    update.message.reply_text(f"All information is erased:", reply_markup=markup2)
    context.user_data.clear()
    start(update, context)
    return ConversationHandler.END


def main():
    """
    The heart of the bot. 
    Keeps track of how program should run.
    Here you specify the token gotten from the BotFather,
    i.e the token for your bot. NB: Keep it secret.
    
    Updater class employs the telegram.ext.Dispatcher and 
    provides a front-end to the bot for the users. 
    So, you only need to focus on backend side.
    
    The ConversationHandler holds a conversation with a single
     user by managing four collections of other handlers
    :return: 
    """
    # Create the Updater and pass it your bot's token.
    pp = PicklePersistence(filename='reloroboty')
    updater = Updater(TOKEN, persistence=pp, use_context=True)

    # Get the dispatcher to register handlers
    dp = updater.dispatcher
    cul_handler = ConversationHandler(
        entry_points=[CommandHandler('Buy_DMT2', args)],
        states={
            CHOOSING: [
                MessageHandler(
                    Filters.regex('^(Public_key|Quantity|Secret_Key|Note)$'),
                    regular_choice), CommandHandler('Done', buy_token)
            ],
            TYPING_CHOICE: [
                MessageHandler(
                    Filters.text
                    & ~(Filters.command | Filters.regex('^Done$')),
                    regular_choice)
            ],
            TYPING_REPLY: [
                MessageHandler(
                    Filters.text
                    & ~(Filters.command | Filters.regex('^Done$')),
                    received_information,
                )
            ],
        },
        fallbacks=[MessageHandler(Filters.regex('^Done$'), done)],
    )

    conv_handler = ConversationHandler(
        entry_points=[CommandHandler('Others', inputcateg)],
        states={
            STARTING: [
                MessageHandler(Filters.regex('^(GetMnemonic|Account_balance|Get_Alc_status|Get_PK)$'),
                               init_choice)
            ],

            GETPK: [
                MessageHandler(
                    Filters.regex('^(Mnemonic)$'), otherwise), CommandHandler('Get_PK', getPK)
            ],
            GETMNEMONIC: [
                MessageHandler(
                    Filters.regex('^(Private_key)$'), otherwise),
                CommandHandler('GetMnemonic', get_mnemonics_from_sk)
            ],
            ACCOUNTBAL: [
                MessageHandler(
                    Filters.regex('^(Public_key)$'), otherwise),
                CommandHandler('Account_balance', query_balance)
            ],

            GETALCSTAT: [
                MessageHandler(
                    Filters.regex('^(Public-Key)$'), otherwise),
                CommandHandler('Get_account_status', account_status)
            ],

            TYPING_REPLY_2: [
                MessageHandler(
                    Filters.text
                    & ~(Filters.command | Filters.regex('^Done$')),
                    received_information_2,
                )
            ],
        },
        fallbacks=[CommandHandler('Done', done)],
    )

    dp.add_handler(cul_handler)
    dp.add_handler(conv_handler)

    dp.add_handler(CommandHandler('start', start))
    dp.add_handler(CommandHandler('Others', inputcateg))
    dp.add_handler(CommandHandler('GetAlc', create_account))
    dp.add_handler(CommandHandler('Cancel', cancel))
    dp.add_handler(CommandHandler('About', aboutUs))
    dp.add_handler(CommandHandler('Help', help_command))
    dp.add_handler(CommandHandler('Main_menu', main_menu))
    dp.add_handler(CommandHandler('Get_My_Address', getAddress))
    dp.add_handler(CommandHandler('Account_balance', inputcateg))
    dp.add_handler(CommandHandler('Get_Alc_status', inputcateg))
    dp.add_handler(CommandHandler('GetMnemonic', inputcateg))
    dp.add_handler(CommandHandler('Get_PK', inputcateg))
    dp.add_handler(CommandHandler('Others', inputcateg))
    dp.add_handler(CommandHandler('Buy_DMT2', args))


    # Start the Bot
    updater.start_polling()

    # Run the bot until you press Ctrl-C or the process receives SIGINT,
    # SIGTERM or SIGABRT. This should be used most of the time, since
    # start_polling() is non-blocking and will stop the bot gracefully.
    updater.idle()


if __name__ == '__main__':
    main()

Other files

  • I manage the keys and tokens using the .env file which is a function of the Python-dotenv library, so I can conveniently work with some sensitive information while pushing to version control.
  • The .env file is added to the .gitignore so it will not be visible when pushed to Github.
  • You can view the project description from the  README.md file.
  • reloroboty is generated when the program is initially launch. It is cache-like that updates the bot, that is, ensuring consistency for the bot. Information like the bot data, user data etc are kept in this file. But it not editable or available for viewing. You can only reference the content while within Telegram scope, more reason we specify “update” and “context” as parameters for every function, otherwise we cannot access needed data.

Program logic/Execution flow
18a14548e9b74c277fcfc8316d74695980d826f43c50af27ae20831e5b858289.jpeg

If your workspace is set up properly, and you followed the steps as highlighted, your bot should work properly. Run the main.py and start the bot. You should see the window as displayed below. For live demo, watch the video. Update your repository with the code. Git helps you to do that. Download if you don’t have one. I have deployed this bot and its live on Google App Engine. You can test it by searching for algobot20 on Telegram or hit this link and Initiate conversation with /start or /Main_menu. Do well to leave a comment if you have any. Visit Algorand developer site for more information on the different available SDKs

Full code is available on github.
19c9909414e9069832e72028e7610ec6400953e500143c59efa9e9f08b8ade75.png

Deployment

Now that we are done with the coding, let’s deploy it so anyone can interact with it. There are couple of ways you can host your application,  for demonstration, we will use Google cloud computing service (free hosting). However, there are limitation to the kind of bot you can deploy for free. Hosting bot that consumes more storage increases the bandwidth hence you be charged. If you already own a Google account, follow the steps in the video below.

The video is unavailable at the moment. I will update it shortly.

You may also like...

3 Responses

  1. escort bayan says:

    Your style is unique in comparison to other folks I have read stuff from. Thanks for posting when you have the opportunity, Guess I will just book mark this site. Jessamine Neddy Kamat

  2. erotik says:

    Hi there, after reading this amazing article i am as well cheerful to share my know-how here with mates. Romonda Addy Herwick

  3. Hey there. I found your blog by means of Google at the same time as searching for a similar topic, your site got here up. It appears great. I have bookmarked it in my google bookmarks to come back then. Regan Kerby Fidelio

Leave a Reply

Your email address will not be published. Required fields are marked *