Storefront bot with payments in TON
In this article, we'll guide you through the process of accepting payments in a Telegram bot.
📖 What you'll learn
In this article, you'll learn how to:
- create a Telegram bot using Python + Aiogram
- work with the public TON API (TON Center)
- work with SQlite database
And finally: how to accept payments in a Telegram bot with the knowledge from previous steps.
📚 Before we begin
Make sure you have installed the latest version of Python and have installed the following packages:
- aiogram
- requests
- sqlite3
🚀 Let's get started!
We'll follow the order below:
- Work with SQlite database
- Work with the public TON API (TON Center)
- Create a Telegram bot using Python + Aiogram
- Profit!
Let's create the following four files in our project directory:
telegram-bot
├── config.json
├── main.py
├── api.py
└── db.py
Config
In config.json
we'll store our bot token and our public TON API key.
{
"BOT_TOKEN": "Your bot token",
"MAINNET_API_TOKEN": "Your mainnet api token",
"TESTNET_API_TOKEN": "Your testnet api token",
"MAINNET_WALLET": "Your mainnet wallet",
"TESTNET_WALLET": "Your testnet wallet",
"WORK_MODE": "testnet"
}
In config.json
we decide which network we'll use: testnet
or mainnet
.
Database
Create a database
This example uses a local Sqlite database.
Create db.py
.
To start working with the database, we need to import the sqlite3 module and some modules for working with time.
import sqlite3
import datetime
import pytz
sqlite3
—module for working with sqlite databasedatetime
—module for working with timepytz
—module for working with timezones
Next, we need to create a connection to the database and a cursor to work with it:
locCon = sqlite3.connect('local.db', check_same_thread=False)
cur = locCon.cursor()
If the database does not exist, it will be created automatically.
Now we can create tables. We have two of them.
Transactions:
CREATE TABLE transactions (
source VARCHAR (48) NOT NULL,
hash VARCHAR (50) UNIQUE
NOT NULL,
value INTEGER NOT NULL,
comment VARCHAR (50)
);
source
—payer's wallet addresshash
—transaction hashvalue
—transaction valuecomment
—transaction comment
Users:
CREATE TABLE users (
id INTEGER UNIQUE
NOT NULL,
username VARCHAR (33),
first_name VARCHAR (300),
wallet VARCHAR (50) DEFAULT none
);
id
—Telegram user IDusername
—Telegram usernamefirst_name
—Telegram user's first namewallet
—user wallet address
In the users
table we store users :) Their Telegram ID, @username,
first name, and wallet. The wallet is added to the database on the first
successful payment.
The transactions
table stores verified transactions.
To verify a transaction, we need the hash, source, value and comment.
To create these tables, we need to run the following function:
cur.execute('''CREATE TABLE IF NOT EXISTS transactions (
source VARCHAR (48) NOT NULL,
hash VARCHAR (50) UNIQUE
NOT NULL,
value INTEGER NOT NULL,
comment VARCHAR (50)
)''')
locCon.commit()
cur.execute('''CREATE TABLE IF NOT EXISTS users (
id INTEGER UNIQUE
NOT NULL,
username VARCHAR (33),
first_name VARCHAR (300),
wallet VARCHAR (50) DEFAULT none
)''')
locCon.commit()
This code will create the tables if they are not already created.
Work with database
Let's analyze the situation: User made a transaction. How to verify it? How to make sure that the same transaction is not confirmed twice?
There is a body_hash in transactions, with the help of which we can easily understand whether there is a transaction in the database or not.
We add transactions to the database in which we are sure. The check_transaction
function checks whether the found transaction is in the database or not.
add_v_transaction
adds transaction to the transactions table.
def add_v_transaction(source, hash, value, comment):
cur.execute("INSERT INTO transactions (source, hash, value, comment) VALUES (?, ?, ?, ?)",
(source, hash, value, comment))
locCon.commit()
def check_transaction(hash):
cur.execute(f"SELECT hash FROM transactions WHERE hash = '{hash}'")
result = cur.fetchone()
if result:
return True
return False
check_user
checks if the user is in the database and adds him if not.
def check_user(user_id, username, first_name):
cur.execute(f"SELECT id FROM users WHERE id = '{user_id}'")
result = cur.fetchone()
if not result:
cur.execute("INSERT INTO users (id, username, first_name) VALUES (?, ?, ?)",
(user_id, username, first_name))
locCon.commit()
return False
return True
The user can store a wallet in the table. It is added with the first successful purchase. The v_wallet
function checks if the user has an associated wallet. If there is, then returns it. If not, then adds.
def v_wallet(user_id, wallet):
cur.execute(f"SELECT wallet FROM users WHERE id = '{user_id}'")
result = cur.fetchone()
if result[0] == "none":
cur.execute(
f"UPDATE users SET wallet = '{wallet}' WHERE id = '{user_id}'")
locCon.commit()
return True
else:
return result[0]
get_user_wallet
simply returns the user's wallet.
def get_user_wallet(user_id):
cur.execute(f"SELECT wallet FROM users WHERE id = '{user_id}'")
result = cur.fetchone()
return result[0]
get_user_payments
returns the user's payments list.
This function checks if the user has a wallet. If he has, then it returns the payment list.
def get_user_payments(user_id):
wallet = get_user_wallet(user_id)
if wallet == "none":
return "You have no wallet"
else:
cur.execute(f"SELECT * FROM transactions WHERE source = '{wallet}'")
result = cur.fetchall()
tdict = {}
tlist = []
try:
for transaction in result:
tdict = {
"value": transaction[2],
"comment": transaction[3],
}
tlist.append(tdict)
return tlist
except:
return False
API
We have the ability to interact with the blockchain using third-party APIs provided by some network members. With these services, developers can skip the step of running their own node and customizing their API.
Required Requests
In fact, what do we need to confirm that the user has transferred the required amount to us?
We just need to look at the latest incoming transfers to our wallet and find among them a transaction from the right address with the right amount (and possibly a unique comment).
For all of this, TON Center has a getTransactions
method.
getTransactions
By default, if we apply it, we will get the last 10 transactions. However, we can also indicate that we need more, but this will slightly increase the time of a response. And, most likely, you do not need so much.
If you want more, then each transaction has lt
and hash
. You can look at, for example, 30 transactions and if the right one was not found among them, then take lt
and hash
from the last one and add them to the request.
So you get the next 30 transactions and so on.
For example, there is a wallet in the test network EQAVKMzqtrvNB2SkcBONOijadqFZ1gMdjmzh1Y3HB1p_zai5
, it has some transactions:
Using a query we will get the response that contains two transactions (some of the information that is not needed now has been hidden, you can see the full answer at the link above).
{
"ok": true,
"result": [
{
"transaction_id": {
"lt": "1944556000003",
"hash": "swpaG6pTBXwYI2024NAisIFp59Fw3k1DRQ5fa5SuKAE="
},
"in_msg": {
"source": "EQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aJ9R",
"destination": "EQAVKMzqtrvNB2SkcBONOijadqFZ1gMdjmzh1Y3HB1p_zai5",
"value": "1000000000",
"body_hash": "kBfGYBTkBaooeZ+NTVR0EiVGSybxQdb/ifXCRX5O7e0=",
"message": "Sea breeze 🌊"
},
"out_msgs": []
},
{
"transaction_id": {
"lt": "1943166000003",
"hash": "hxIQqn7lYD/c/fNS7W/iVsg2kx0p/kNIGF6Ld0QEIxk="
},
"in_msg": {
"source": "EQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aJ9R",
"destination": "EQAVKMzqtrvNB2SkcBONOijadqFZ1gMdjmzh1Y3HB1p_zai5",
"value": "1000000000",
"body_hash": "7iirXn1RtliLnBUGC5umIQ6KTw1qmPk+wwJ5ibh9Pf0=",
"message": "Spring forest 🌲"
},
"out_msgs": []
}
]
}
We have received the last two transactions from this address. When adding lt
and hash
to the query, we will again receive two transactions. However, the second one will become the next one in a row. That is, we will get the second and third transactions for this address.
{
"ok": true,
"result": [
{
"transaction_id": {
"lt": "1943166000003",
"hash": "hxIQqn7lYD/c/fNS7W/iVsg2kx0p/kNIGF6Ld0QEIxk="
},
"in_msg": {
"source": "EQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aJ9R",
"destination": "EQAVKMzqtrvNB2SkcBONOijadqFZ1gMdjmzh1Y3HB1p_zai5",
"value": "1000000000",
"body_hash": "7iirXn1RtliLnBUGC5umIQ6KTw1qmPk+wwJ5ibh9Pf0=",
"message": "Spring forest 🌲"
},
"out_msgs": []
},
{
"transaction_id": {
"lt": "1845458000003",
"hash": "k5U9AwIRNGhC10hHJ3MBOPT//bxAgW5d9flFiwr1Sao="
},
"in_msg": {
"source": "EQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aJ9R",
"destination": "EQAVKMzqtrvNB2SkcBONOijadqFZ1gMdjmzh1Y3HB1p_zai5",
"value": "1000000000",
"body_hash": "XpTXquHXP64qN6ihHe7Tokkpy88tiL+5DeqIrvrNCyo=",
"message": "Second"
},
"out_msgs": []
}
]
}
The request will look like this.
We will also need a method detectAddress
.
Here is an example of a Tonkeeper wallet address on testnet: kQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aCTb
. If we look for the transaction in the explorer, instead of the above address, there is: EQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aJ9R
.
This method returns us the “right” address.
{
"ok": true,
"result": {
"raw_form": "0:b3409241010f85ac415cbf13b9b0dc6157d09a39d2bd0827eadb20819f067868",
"bounceable": {
"b64": "EQCzQJJBAQ+FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aJ9R",
"b64url": "EQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aJ9R"
},
"non_bounceable": {
"b64": "UQCzQJJBAQ+FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aMKU",
"b64url": "UQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aMKU"
}
}
}
We need b64url
.
This method allows us to validate the user's address.
For the most part, that's all we need.
API requests and what to do with them
Let's go back to the IDE. Create the file api.py
.
Import the necessary libraries.
import requests
import json
# We import our db module, as it will be convenient to add from here
# transactions to the database
import db
requests
—to make requests to the APIjson
—to work with jsondb
—to work with our sqlite database
Let's create two variables for storing the start of the requests.
# This is the beginning of our requests
MAINNET_API_BASE = "https://toncenter.com/api/v2/"
TESTNET_API_BASE = "https://testnet.toncenter.com/api/v2/"
Get all API tokens and wallets from the config.json file.
# Find out which network we are working on
with open('config.json', 'r') as f:
config_json = json.load(f)
MAINNET_API_TOKEN = config_json['MAINNET_API_TOKEN']
TESTNET_API_TOKEN = config_json['TESTNET_API_TOKEN']
MAINNET_WALLET = config_json['MAINNET_WALLET']
TESTNET_WALLET = config_json['TESTNET_WALLET']
WORK_MODE = config_json['WORK_MODE']
Depending on the network, we take the necessary data.
if WORK_MODE == "mainnet":
API_BASE = MAINNET_API_BASE
API_TOKEN = MAINNET_API_TOKEN
WALLET = MAINNET_WALLET
else:
API_BASE = TESTNET_API_BASE
API_TOKEN = TESTNET_API_TOKEN
WALLET = TESTNET_WALLET
Our first request function detectAddress
.
def detect_address(address):
url = f"{API_BASE}detectAddress?address={address}&api_key={API_TOKEN}"
r = requests.get(url)
response = json.loads(r.text)
try:
return response['result']['bounceable']['b64url']
except:
return False
At the input, we have the estimated address, and at the output, we have either the "correct" address necessary for us to do further work or False.
You may notice that an API key has appeared at the end of the request. It is needed to remove the limit on the number of requests to the API. Without it, we are limited to one request per second.
Here is next function for getTransactions
:
def get_address_transactions():
url = f"{API_BASE}getTransactions?address={WALLET}&limit=30&archival=true&api_key={API_TOKEN}"
r = requests.get(url)
response = json.loads(r.text)
return response['result']
This function returns the last 30 transactions to our WALLET
.
Here you can see archival=true
. It is needed so that we only take transactions from a node with a complete history of the blockchain.
At the output, we get a list of transactions—[0,1,...,29]. List of dictionaries in short.
And finally the last function:
def find_transaction(user_wallet, value, comment):
# Get the last 30 transactions
transactions = get_address_transactions()
for transaction in transactions:
# Select the incoming "message" - transaction
msg = transaction['in_msg']
if msg['source'] == user_wallet and msg['value'] == value and msg['message'] == comment:
# If all the data match, we check that this transaction
# we have not verified before
t = db.check_transaction(msg['body_hash'])
if t == False:
# If not, we write in the table to the verified
# and return True
db.add_v_transaction(
msg['source'], msg['body_hash'], msg['value'], msg['message'])
print("find transaction")
print(
f"transaction from: {msg['source']} \nValue: {msg['value']} \nComment: {msg['message']}")
return True
# If this transaction is already verified, we check the rest, we can find the right one
else:
pass
# If the last 30 transactions do not contain the required one, return False
# Here you can add code to see the next 29 transactions
# However, within the scope of the Example, this would be redundant.
return False
At the input are the “correct” wallet address, amount and comment. If the intended incoming transaction is found, the output is True; otherwise, it is False.
Telegram bot
First, let's create the basis for a bot.
Imports
In this part, we will import the necessary libraries.
From aiogram
we need Bot
, Dispatcher
, types
and executor
.
from aiogram import Bot, Dispatcher, executor, types
MemoryStorage
is needed for the temporary storage of information.
FSMContext
, State
, and StatesGroup
are needed for working with the state machine.
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram.dispatcher import FSMContext
from aiogram.dispatcher.filters.state import State, StatesGroup
json
is needed to work with json files. logging
is needed to log errors.
import json
import logging