자체 잔액을 가진 봇
이 글에서는 TON으로 결제를 받는 간단한 텔레그램 봇을 만들어보겠습니다.
🦄 어떻게 보이나요
봇은 다음과 같이 보일 것입니다:
소스 코드
소스는 GitHub에서 확인할 수 있습니다:
📖 배울 내용
다음 내용을 배우게 됩니다:
- Python3에서 Aiogram을 사용하여 텔레그램 봇 만들기
- SQLITE 데이터베이스 다루기
- 공개 TON API 사용하기
✍️ 시작하기 전 준비사항
아직 설치하지 않았다면 Python을 설치하세요.
또한 다음 PyPi 라이브러리들이 필요합니다:
- aiogram
- requests
터미널에서 다음 명령어로 한 번에 설치할 수 있습니다.
pip install aiogram==2.21 requests
🚀 시작하기!
봇을 위한 디렉토리를 만들고 다음 네 개의 파일을 생성합니다:
bot.py
- 텔레그램 봇을 실행하는 프로그램config.py
- 설정 파일db.py
- sqlite3 데이터베이스와 상호작용하는 모듈ton.py
- TON에서 결제를 처리하는 모듈
디렉토리는 다음과 같이 보여야 합니다:
my_bot
├── bot.py
├── config.py
├── db.py
└── ton.py
이제 코드를 작성해봅시다!
설정
가장 작은 파일인 config.py
부터 시작하겠습니다. 몇 가지 파라미터만 설정하면 됩니다.
config.py
BOT_TOKEN = 'YOUR BOT TOKEN'
DEPOSIT_ADDRESS = 'YOUR DEPOSIT ADDRESS'
API_KEY = 'YOUR API KEY'
RUN_IN_MAINNET = True # Switch True/False to change mainnet to testnet
if RUN_IN_MAINNET:
API_BASE_URL = 'https://toncenter.com'
else:
API_BASE_URL = 'https://testnet.toncenter.com'
처음 세 줄의 값을 채워넣어야 합니다:
BOT_TOKEN
은 봇 생성 후 받을 수 있는 텔레그램 봇 토큰입니다.DEPOSIT_ADDRESS
는 모든 결제를 받을 프로젝트의 지갑 주소입니다. 새로운 TON Wallet을 만들고 주소를 복사하면 됩니다.API_KEY
는 이 봇에서 받을 수 있는 TON Center의 API 키입니다.
봇을 테스트넷이나 메인넷에서 실행할지도 선택할 수 있습니다(4번째 줄).
설정 파일은 이게 전부이니 다음으로 넘어갑시다!
데이터베이스
이제 봇의 데이터베이스를 다룰 db.py
파일을 수정해봅시다.
sqlite3 라이브러리를 임포트합니다.
import sqlite3
데이터베이스 연결과 커서를 초기화합니다(db.sqlite
대신 원하는 파일명을 선택할 수 있습니다).
con = sqlite3.connect('db.sqlite')
cur = con.cursor()
사용자 정보(이 경우 잔액)를 저장하기 위해 사용자 ID와 잔액 열이 있는 "Users" 테이블을 만듭니다.
cur.execute('''CREATE TABLE IF NOT EXISTS Users (
uid INTEGER,
balance INTEGER
)''')
con.commit()
이제 데이터베이스 작업을 위한 몇 가지 함수를 선언해야 합니다.
add_user
함수는 새로운 사용자를 데이터베이스에 추가하는 데 사용됩니다.
def add_user(uid):
# new user always has balance = 0
cur.execute(f'INSERT INTO Users VALUES ({uid}, 0)')
con.commit()
check_user
함수는 사용자가 데이터베이스에 있는지 확인하는 데 사용됩니다.
def check_user(uid):
cur.execute(f'SELECT * FROM Users WHERE uid = {uid}')
user = cur.fetchone()
if user:
return True
return False
add_balance
함수는 사용자의 잔액을 증가시키는 데 사용됩니다.
def add_balance(uid, amount):
cur.execute(f'UPDATE Users SET balance = balance + {amount} WHERE uid = {uid}')
con.commit()
get_balance
함수는 사용자의 잔액을 조회하는 데 사용됩니다.
def get_balance(uid):
cur.execute(f'SELECT balance FROM Users WHERE uid = {uid}')
balance = cur.fetchone()[0]
return balance
이게 db.py
파일의 전부입니다!
이제 봇의 다른 구성 요소에서 이 네 가지 함수를 사용하여 데이터베이스를 다룰 수 있습니다.
TON Center API
ton.py
파일에서는 모든 새로운 입금을 처리하고, 사용자 잔액을 증가시키고, 사용자에게 알림을 보내는 함수를 선언할 것입니다.
getTransactions 메소드
TON Center API를 사용할 것입니다. 문서는 여기서 확인할 수 있습니다: https://toncenter.com/api/v2/
주어진 계정의 최근 거래 정보를 얻기 위해 getTransactions 메소드가 필요합니다.
이 메소드가 어떤 입력 파라미터를 받고 무엇을 반환하는지 살펴봅시다.
address
필드만 필수이지만, 반환받을 거래 수를 지정하기 위해 limit
필드도 필요합니다.
이제 TON Center 웹사이트에서 이 메소드를 실존하는 지갑 주소로 실행해보고 출력에서 무엇을 얻어야 하는지 이해해봅시다.
{
"ok": true,
"result": [
{
...
},
{
...
}
]
}
모든 것이 정상이면 ok
필드가 true
로 설정되고, limit
개수만큼의 최근 거래 목록이 있는 result
배열이 있습니다. 이제 단일 거래를 살펴봅시다:
{
"@type": "raw.transaction",
"utime": 1666648337,
"data": "...",
"transaction_id": {
"@type": "internal.transactionId",
"lt": "32294193000003",
"hash": "ez3LKZq4KCNNLRU/G4YbUweM74D9xg/tWK0NyfuNcxA="
},
"fee": "105608",
"storage_fee": "5608",
"other_fee": "100000",
"in_msg": {
"@type": "raw.message",
"source": "EQBIhPuWmjT7fP-VomuTWseE8JNWv2q7QYfsVQ1IZwnMk8wL",
"destination": "EQBKgXCNLPexWhs2L79kiARR1phGH1LwXxRbNsCFF9doc2lN",
"value": "100000000",
"fwd_fee": "666672",
"ihr_fee": "0",
"created_lt": "32294193000002",
"body_hash": "tDJM2A4YFee5edKRfQWLML5XIJtb5FLq0jFvDXpv0xI=",
"msg_data": {
"@type": "msg.dataText",
"text": "SGVsbG8sIHdvcmxkIQ=="
},
"message": "Hello, world!"
},
"out_msgs": []
}
정확한 거래를 식별하는 데 도움이 되는 정보는 transaction_id
필드에 저장되어 있습니다. 어떤 거래가 더 일찍 발생했고 어떤 거래가 더 늦게 발생했는지 이해하기 위해 lt
필드가 필요합니다.
코인 전송에 대한 정보는 in_msg
필드에 있습니다. 여기서 value
와 message
가 필요합니다.
이제 결제 핸들러를 만들 준비가 되었습니다.
코드에서 API 요청 보내기
먼저 필요한 라이브러리와 이전에 만든 두 파일인 config.py
와 db.py
를 임포트합니다.
import requests
import asyncio
# Aiogram
from aiogram import Bot
from aiogram.types import ParseMode
# We also need config and database here
import config
import db
결제 처리를 어떻게 구현할 수 있을지 생각해봅시다.
몇 초마다 API를 호출하여 우리 지갑 주소로 새로운 거래가 있는지 확인할 수 있습니다.
이를 위해서는 마지막으로 처리된 거래가 무엇인지 알아야 합니다. 가장 간단한 방법 은 해당 거래에 대한 정보를 파일에 저장하고 새로운 거래를 처리할 때마다 업데이트하는 것입니다.
파일에 어떤 거래 정보를 저장할까요? 사실 논리적 시간인 lt
값만 저장하면 됩니다.
이 값으로 어떤 거래를 처리해야 하는지 이해할 수 있습니다.
따라서 새로운 비동기 함수를 정의해야 합니다. start
라고 부르겠습니다. 이 함수가 비동기여야 하는 이유는 무엇일까요? 텔레그램 봇을 위한 Aiogram 라이브러리도 비동기이기 때문에, 나중에 비동기 함수와 작업하기가 더 쉬울 것이기 때문입니다.
우리의 start
함수는 다음과 같이 보일 것입니다:
async def start():
try:
# Try to load last_lt from file
with open('last_lt.txt', 'r') as f:
last_lt = int(f.read())
except FileNotFoundError:
# If file not found, set last_lt to 0
last_lt = 0
# We need the Bot instance here to send deposit notifications to users
bot = Bot(token=config.BOT_TOKEN)
while True:
# Here we will call API every few seconds and fetch new transactions.
...
이제 while 루프의 본문을 작성해봅시다. 여기서 매 몇 초마다 TON Center API를 호출해야 합니다.
while True:
# 2 Seconds delay between checks
await asyncio.sleep(2)
# API call to TON Center that returns last 100 transactions of our wallet
resp = requests.get(f'{config.API_BASE_URL}/api/v2/getTransactions?'
f'address={config.DEPOSIT_ADDRESS}&limit=100&'
f'archival=true&api_key={config.API_KEY}').json()
# If call was not successful, try again
if not resp['ok']:
continue
...
requests.get
으로 호출한 후, API의 응답을 포함하는 resp
변수가 있습니다. resp
는 객체이고 resp['result']
는 우리 주소의 마지막 100개 거래가 있는 리스트입니다.
이제 이러한 거래들을 반복하면서 새로운 것들을 찾아봅시다.
while True:
...
# Iterating over transactions
for tx in resp['result']:
# LT is Logical Time and Hash is hash of our transaction
lt, hash = int(tx['transaction_id']['lt']), tx['transaction_id']['hash']
# If this transaction's logical time is lower than our last_lt,
# we already processed it, so skip it
if lt <= last_lt:
continue
# at this moment, `tx` is some new transaction that we haven't processed yet
...
새로운 거래를 어떻게 처리할까요? 다음을 수행해야 합니다:
- 누가 보냈는지 이해하기
- 해당 사용자의 잔액을 증가시키기
- 사용자에게 입금 알림 보내기
다음은 이 모든 것을 수행하는 코드입니다:
while True:
...
for tx in resp['result']:
...
# at this moment, `tx` is some new transaction that we haven't processed yet
value = int(tx['in_msg']['value'])
if value > 0:
uid = tx['in_msg']['message']
if not uid.isdigit():
continue
uid = int(uid)
if not db.check_user(uid):
continue
db.add_balance(uid, value)
await bot.send_message(uid, 'Deposit confirmed!\n'
f'*+{value / 1e9:.2f} TON*',
parse_mode=ParseMode.MARKDOWN)
이것이 무엇을 하는지 이해해봅시다.
코인 전송에 대한 모든 정보는 tx['in_msg']
에 있습니다. 여기서는 'value'와 'message' 필드만 필요합니다.
먼저 값이 0보다 큰지 확인하고 그런 경우에만 계속합니다.
그런 다음 이체에 우리 봇의 사용자 ID가 포함된 코멘트(tx['in_msg']['message']
)가 있기를 기대하므로, 유효한 숫자인지와 그 UID가 데이터베이스에 존재하는지 확인합니다.
이러한 간단한 확인 후에, 입금 금액이 있는 value
변수와 이 입금을 한 사용자의 ID가 있는 uid
변수가 있습니다. 따라서 해당 계정에 자금을 추가하고 알림 메시지를 보낼 수 있습니다.
또한 기본적으로 value는 나노톤 단위라는 점에 유의하세요. 따라서 10억으로 나눠야 합니다. 알림 줄에서 이렇게 합니다:
{value / 1e9:.2f}
여기서 값을 1e9
(10억)로 나누고 소수점 이하 두 자리만 남겨 사용자에게 친숙한 형식으로 보여줍니다.
훌륭합니다! 이제 프로그램이 새 로운 거래를 처리하고 사용자에게 입금을 알릴 수 있습니다. 하지만 이전에 사용했던 lt
를 저장하는 것을 잊지 말아야 합니다. 새로운 거래가 처리되었으므로 마지막 lt
를 업데이트해야 합니다.
간단합니다:
while True:
...
for tx in resp['result']:
...
# we have processed this tx
# lt variable here contains LT of the last processed transaction
last_lt = lt
with open('last_lt.txt', 'w') as f:
f.write(str(last_lt))
이것으로 ton.py
파일이 완성되었습니다!
봇의 3/4이 완성되었고, 이제 봇 자체에 몇 개의 버튼이 있는 사용자 인터페이스만 만들면 됩니다.
텔레그램 봇
초기화
bot.py
파일을 열고 필요한 모든 모듈을 임포트합니다.
# Logging module
import logging
# Aiogram imports
from aiogram import Bot, Dispatcher, types
from aiogram.dispatcher.filters import Text
from aiogram.types import ParseMode, ReplyKeyboardMarkup, KeyboardButton, \
InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.utils import executor
# Local modules to work with the Database and TON Network
import config
import ton
import db
나중에 디버깅을 위해 무슨 일이 일어나는지 볼 수 있도록 프로그램에 로깅을 설정합시다.
logging.basicConfig(level=logging.INFO)
이제 Aiogram으로 봇 객체와 디스패처를 초기화해야 합니다.
bot = Bot(token=config.BOT_TOKEN)
dp = Dispatcher(bot)
여기서 튜토리얼 시작 부분에서 만든 config의 BOT_TOKEN
을 사용합니다.
봇을 초기화했지만 아직 비어 있습니다. 사용자와의 상호작용을 위한 함수를 추가해야 합니다.
메시지 핸들러
/start 명령
/start
와 /help
명령 핸들러부터 시작해봅시다. 이 함수는 사용자가 처음으로 봇을 시작하거나, 재시작하거나, /help
명령을 사용할 때 호출됩니다.
@dp.message_handler(commands=['start', 'help'])
async def welcome_handler(message: types.Message):
uid = message.from_user.id # Not neccessary, just to make code shorter
# If user doesn't exist in database, insert it
if not db.check_user(uid):
db.add_user(uid)
# Keyboard with two main buttons: Deposit and Balance
keyboard = ReplyKeyboardMarkup(resize_keyboard=True)
keyboard.row(KeyboardButton('Deposit'))
keyboard.row(KeyboardButton('Balance'))
# Send welcome text and include the keyboard
await message.answer('Hi!\nI am example bot '
'made for [this article](docs.ton.org/v3/guidelines/dapps/tutorials/telegram-bot-examples/accept-payments-in-a-telegram-bot-2).\n'
'My goal is to show how simple it is to receive '
'payments in Toncoin with Python.\n\n'
'Use keyboard to test my functionality.',
reply_markup=keyboard,
parse_mode=ParseMode.MARKDOWN)
환영 메시지는 원하는 대로 할 수 있습니다. 키보드 버튼도 아무 텍스트나 가능하지만, 이 예제에서는 봇의 기능을 가장 명확하게 보여주는 방식으로 라벨을 붙였습니다: 입금
과 잔액
.
잔액 버튼
이제 사용자는 봇을 시작하고 두 개의 버튼이 있는 키보드를 볼 수 있습니다. 하지만 이들 중 하나를 호출한 후에는 아무 응답도 받지 못할 것입니다. 아직 그들을 위한 함수를 만들지 않았기 때문입니다.
그래서 잔액을 요청하는 함수를 추가해봅시다.
@dp.message_handler(commands='balance')
@dp.message_handler(Text(equals='balance', ignore_case=True))
async def balance_handler(message: types.Message):
uid = message.from_user.id
# Get user balance from database
# Also don't forget that 1 TON = 1e9 (billion) Nanoton
user_balance = db.get_balance(uid) / 1e9
# Format balance and send to user
await message.answer(f'Your balance: *{user_balance:.2f} TON*',
parse_mode=ParseMode.MARKDOWN)
매우 간단합니다. 데이터베이스에서 잔액을 가져와서 사용자에게 메시지를 보내기만 하면 됩니다.
입금 버튼
그리고 두 번째 입금
버튼은 어떨까요? 여기 그것을 위한 함수가 있습니다:
@dp.message_handler(commands='deposit')
@dp.message_handler(Text(equals='deposit', ignore_case=True))
async def deposit_handler(message: types.Message):
uid = message.from_user.id
# Keyboard with deposit URL
keyboard = InlineKeyboardMarkup()
button = InlineKeyboardButton('Deposit',
url=f'ton://transfer/{config.DEPOSIT_ADDRESS}&text={uid}')
keyboard.add(button)
# Send text that explains how to make a deposit into bot to user
await message.answer('It is very easy to top up your balance here.\n'
'Simply send any amount of TON to this address:\n\n'
f'`{config.DEPOSIT_ADDRESS}`\n\n'
f'And include the following comment: `{uid}`\n\n'
'You can also deposit by clicking the button below.',
reply_markup=keyboard,
parse_mode=ParseMode.MARKDOWN)
여기서 하는 일도 이해하기 쉽습니다.
ton.py
파일에서 UID로 코멘트를 달아 어떤 사용자가 입금했는지 확인했던 것을 기억하시나요? 이제 여기 봇에서는 사용자에게 자신의 UID가 포함된 코멘트와 함께 거래를 보내달라고 요청해야 합니다.
봇 시작
이제 bot.py
에서 해야 할 일은 봇 자체를 실행하고 ton.py
의 start
함수도 실행하는 것뿐입니다.
if __name__ == '__main__':
# Create Aiogram executor for our bot
ex = executor.Executor(dp)
# Launch the deposit waiter with our executor
ex.loop.create_task(ton.start())
# Launch the bot
ex.start_polling()
이 시점에서 봇에 필요한 모든 코드를 작성했습니다. 모든 것을 올바르게 했다면 터미널에서 python my-bot/bot.py
명령으로 실행했을 때 작동해야 합니다.
봇이 제대로 작동하지 않는다면, 이 저장소의 코드와 비교해보세요.
참고자료
- ton-footsteps/8의 일부로 TON을 위해 만들어짐
- 작성자: Gusarich (텔레그램 @Gusarich, GitHub Gusarich)