使用 TON 的商店机器人
在这篇文章中,我们将引导你完成在 Telegram 机器人中接受付款的过程。
📖 你将学到什么
在这篇文章中,你将学习如何:
- 使用 Python + Aiogram 创建一个 Telegram 机器人
- 使用公开的 TON API(TON Center)
- 使用 SQlite 数据库
最后:通过前面步骤的知识,在 Telegram 机器人中接受付款。
📚 在我们开始之前
确保你已经安装了最新版本的 Python,并且已经安装了以下包:
- aiogram
- requests
- sqlite3
🚀 我们开始吧!
我们将按照以下顺序操作:
- 使用 SQlite 数据库
- 使用公开的 TON API(TON Center)
- 使用 Python + Aiogram 创建一个 Telegram 机器人
- 盈利!
让我们在项目目录中创建以下四个文件:
telegram-bot
├── config.json
├── main.py
├── api.py
└── db.py
配置
在 config.json
中,我们将存储我们的机器人令牌和我们的公开 TON API 密钥。
{
"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"
}
在 config.json
中,我们决定我们将使用哪个网络:testnet
或 mainnet
。
数据库
创建数据库
这个示例使用本地 Sqlite 数据库。
创建 db.py
。
开始使用数据库,我们需要导入 sqlite3 模块和一些用于处理时间的模块。
import sqlite3
import datetime
import pytz
sqlite3
—用于操作 sqlite 数据库的模块datetime
—用于处理时间的模块pytz
—用于处理时区的模块
接下来,我们需要创建一个数据库的连接和一个用于操作它的游标:
locCon = sqlite3.connect('local.db', check_same_thread=False)
cur = locCon.cursor()
如果数据库不存在,将会自动创建。
现在我们可以创建表格了。我们有两个表格。
交易:
CREATE TABLE transactions (
source VARCHAR (48) NOT NULL,
hash VARCHAR (50) UNIQUE
NOT NULL,
value INTEGER NOT NULL,
comment VARCHAR (50)
);
source
—付款人的钱包地址hash
—交易哈希value
—交易价值comment
—交易备注
用户:
CREATE TABLE users (
id INTEGER UNIQUE
NOT NULL,
username VARCHAR (33),
first_name VARCHAR (300),
wallet VARCHAR (50) DEFAULT none
);
id
—Telegram 用户 IDusername
—Telegram 用户名first_name
—Telegram 用户的名字wallet
—用户钱包地址
在 users
表中,我们存储用户 :) 他们的 Telegram ID、@username、
名字和钱包。第一次成功付款时,钱包将被添加到数据库中。
transactions
表存储已验证的交易。
要验证交易,我们需要哈希、来源、值和备注。
要创建这些表格,我们需要运行以下函数:
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()
如果这些表格还没有被创建,这段代码将会创建它们。
使用数据库
让我们分析一种情况: 用户进行了一笔交易。我们如何验证它?我们如何确保同一笔交易不被二次确认?
交易中有一个 body_hash,通过它我们可以轻松地了解数据库中是否存在该交易。
我们只添加我们确定的交易到数据库。check_transaction
函数检查数据库中是否存在找到的交易。
add_v_transaction
将交易添加到交易表。
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
检查用户是否在数据库中,并且如果不在,则添加他。
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
用户可以在表中存储一个钱包。它是在第一次成功购买时添加的。v_wallet
函数检查用户是否有关联的钱包。如果有,则返回它。如果没有,则添加。
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
简单地返回用户的钱包。
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
返回用户的支付列表。
这个函数检查用户是否有钱包。如果有,则返回支付列表。
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
我们有能力使用一些网络成员提供的第三方 API 与区块链进行交互。通过这些服务,开发者可以跳过运行自己的节点和自定义 API 的步骤。
需要的请求
实际上,我们需要确认用户已经向我们转账了所需金额吗?
我们只需要查看我们钱包的最新进账转账,并在其中找到一笔来自正确地址、正确金额的交易(可能还有一个独特的备注)。
为了所有这一切,TON Center 有一个 getTransactions
方法。
getTransactions
默认情况下,如果我们使用它,我们将获得最后 10 条交易。然而,我们也可以表示我们需要更多,但这会略微增加响应时间。而且,很有可能,你不需要那么多。
如果您想要更多,那么每笔交易都有 lt
和 hash
。您可以查看例如 30 条交易,如果没在其中找到正确的一笔,那么取最后一笔的 lt
和 hash
添加到请求中。
这样您就可以得到下一个 30 条交易,以此类推。
例如,测试网络中有一个钱包 EQAVKMzqtrvNB2SkcBONOijadqFZ1gMdjmzh1Y3HB1p_zai5
,它有一些交易:
使用查询 我们将得到包含两笔交易的响应(现在不需要的一些信息已经被隐藏,完整答案可以在上面的链接中看到)。
{
"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": []
}
]
}
我们从这个地址收到了最后两笔交易。当添加 lt
和 hash
到查询中时,我们将再次收到两笔交易。然而,第二笔将成为下一笔连续的交易。也就是说,我们将获得这个地址的第二笔和第三笔交易。
{
"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": []
}
]
}
请求将看起来像这样。
我们还需要一个方法 detectAddress
。
这是测试网上的 Tonkeeper 钱包地址的一个例子:kQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aCTb
。如果我们在浏览器中查找交易,代替上述地址,有:EQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aJ9R
。
这个方法返回给我们“正确”的地址。
{
"ok": true,
"result": {
"raw_form": "0:b3409241010f85ac415cbf13b9b0dc6157d09a39d2bd0827eadb20819f067868",
"bounceable": {
"b64": "EQCzQJJBAQ+FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aJ9R",
"b64url": "EQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aJ9R"
},
"non_bounceable": {
"b64": "UQCzQJJBAQ+FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aMKU",
"b64url": "UQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aMKU"
}
}
}
我们需要 b64url
。
这个方法让我们能够验证用户的地址。
大部分而言,这就是我们所需要的。
API 请求及其处理方法
让我们回到 IDE。创建文件 api.py
。
导入所需的库。
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
—用来向 API 发送请求json
—用来处理 jsondb
—用来处理我们的 sqlite 数据库
让我们创建两个变量来存储请求的开头。
# This is the beginning of our requests
MAINNET_API_BASE = "https://toncenter.com/api/v2/"
TESTNET_API_BASE = "https://testnet.toncenter.com/api/v2/"
从 config.json 文件中获取所有 API 令牌和钱包。
# 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']
根据网络,我们取所需的数据。
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
我们的第一个请求函数 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
在输入中,我们有预计的地址,输出要么是我们需要的“正确”地址,以便进行进一步的工作,要么是 False。
你可能会注意到请求末尾出现了 API 密钥。它是为了移除对 API 请求数量的限制。没有它,我们被限制为每秒一个请求。
这里是 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']
此函数返回最后 30 次对我们 WALLET
的交易。
这里可以看到 archival=true
。这是因为我们只需要从具有完整区块链历史记录的节点获取交易。
在输出中,我们获得一个交易列表—[0,1,…,29]。简而言之,是字典列表。
最后一个函 数:
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
输入是“正确”的钱包地址、金额和评论。如果找到预期的进账交易,输出为 True;否则为 False。
Telegram 机器人
首先,让我们为机器人创建基础。
导入
在这部分,我们将导入所需的库。
来自 aiogram
,我们需要 Bot
、Dispatcher
、types
和 executor
。
from aiogram import Bot, Dispatcher, executor, types
MemoryStorage
是用于临时存储信息的。
FSMContext
, State
, 和 StatesGroup
用于与状态机工作。
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram.dispatcher import FSMContext
from aiogram.dispatcher.filters.state import State, StatesGroup
json
用来处理 json 文件。logging
用来记录错误。
import json
import logging
api
和 db
是我们自己的文件,稍后我们将填充内容。
import db
import api
配置设置
建议您将如 BOT_TOKEN
和接收付款的钱包等数据存储在一个名为 config.json
的单独文件中,以便于使用。
{
"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"
}
机器人令牌
BOT_TOKEN
是你的 Telegram 机器人令牌,来自 @BotFather
工作模式
在 WORK_MODE
键中,我们将定义机器人的工作模式—在测试网或主网;分别为 testnet
或 mainnet
。
API 令牌
*_API_TOKEN
的 API 令牌可以在 TON Center 机器人处获取:
- 对于主网 — @tonapibot
- 对于测试网 — @tontestnetapibot
将配置连接到我们的机器人
接下来,我们完成机器人设置。
从 config.json
获取机器人工作所需的令牌:
with open('config.json', 'r') as f:
config_json = json.load(f)
BOT_TOKEN = config_json['BOT_TOKEN']
# put wallets here to receive payments
MAINNET_WALLET = config_json['MAINNET_WALLET']
TESTNET_WALLET = config_json['TESTNET_WALLET']
WORK_MODE = config_json['WORK_MODE']
if WORK_MODE == "mainnet":
WALLET = MAINNET_WALLET
else:
# By default, the bot will run on the testnet
WALLET = TESTNET_WALLET
日志记录和机器人设置
logging.basicConfig(level=logging.INFO)
bot = Bot(token=BOT_TOKEN, parse_mode=types.ParseMode.HTML)
dp = Dispatcher(bot, storage=MemoryStorage())
状态
我们需要使用状态将机器人工作流程划分为阶段。我们可以将每个阶段专门用于特定任务。
class DataInput (StatesGroup):
firstState = State()
secondState = State()
WalletState = State()
PayState = State()
详情和示例请参见 Aiogram 文档。
消息处理器(Message handlers)
这是我们将编写机器人交互逻辑的部分。
我们将使用两种类型的处理器:
message_handler
用于处理用户消息。callback_query_handler
用于处理来自内联键盘的回调。
如果我们想处理用户的消息,我们将使用 message_handler
并在函数上方放置 @dp.message_handler
装饰器。在这种情况下,当用户向机器人发送消息时,将调用该函数。
在装饰器中,我们可以指定将在何种条件下调用该函数。例如,如果我们想要在用户发送文本 /start
的消息时调用函数,那么我们将编写以下内容:
@dp.message_handler(commands=['start'])
处理器需要分配给一个异步函数。在这种情况下,我们将使用 async def
语法。async def
语法用于定义将异步调用的函数。
/start
让我们从 /start
命令处理器开始。
@dp.message_handler(commands=['start'], state='*')
async def cmd_start(message: types.Message):
await message.answer(f"WORKMODE: {WORK_MODE}")
# check if user is in database. if not, add him
isOld = db.check_user(
message.from_user.id, message.from_user.username, message.from_user.first_name)
# if user already in database, we can address him differently
if isOld == False:
await message.answer(f"You are new here, {message.from_user.first_name}!")
await message.answer(f"to buy air send /buy")
else:
await message.answer(f"Welcome once again, {message.from_user.first_name}!")
await message.answer(f"to buy more air send /buy")
await DataInput.firstState.set()
在此处理器的装饰器中,我们看到 state='*'
。这意味着无论机器人的状态如何,该处理器都将被调用。如果我们希望处理器仅在机器人处于特定状态时调用,我们将编写 state=DataInput.firstState
。在这种情况下,处理器仅在机器人处于 firstState
状态时被调用。
用户发送 /start
命令后,机器人将使用 db.check_user
函数检查用户是否在数据库中。如果不是,它将添加他。此函数还将返回布尔值,我们可以使用它以不同的方式对待用户。之后,机器人将设置状态为 firstState
。