使用 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"
}