From c264512e64fb66fcb9dfb937b4820de3a894205c Mon Sep 17 00:00:00 2001 From: yungwine Date: Thu, 28 Sep 2023 22:02:43 +0800 Subject: [PATCH 1/4] WIP: add python tc tg bot tutorial --- .../ton-connect/tg-bot-integration-py.md | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 docs/develop/dapps/ton-connect/tg-bot-integration-py.md diff --git a/docs/develop/dapps/ton-connect/tg-bot-integration-py.md b/docs/develop/dapps/ton-connect/tg-bot-integration-py.md new file mode 100644 index 0000000000..a6d5927d5f --- /dev/null +++ b/docs/develop/dapps/ton-connect/tg-bot-integration-py.md @@ -0,0 +1,322 @@ +# TON Connect for Telegram Bots + +In this tutorial, we’ll create a sample telegram bot that supports TON Connect 2.0 authentication using Python TON Connect SDK [pytonconnect](https://github.com/XaBbl4/pytonconnect). +We will analyze connecting a wallet, sending a transaction, getting data about the connected wallet, and disconnecting a wallet. + +## Implementation + +### 1) Installation + +To make bot we are going to use `aiogram` 3.0 Python library. To start integrating TON Connect into your Telegram bot, you need to install the `pytonconnect` package. +You can use pip for this purpose: + +```bash +pip install aiogram +pip install pytonconnect +``` + +### 2) Set up config file +Specify here your [bot token](https://t.me/BotFather), and link to the TON Connect [manifest file](https://github.com/ton-connect/sdk/tree/main/packages/sdk#add-the-tonconnect-manifest). +```python +# config.py + +TOKEN = '1111111111:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' # your bot token here +MANIFEST_URL = 'https://raw.githubusercontent.com/XaBbl4/pytonconnect/main/pytonconnect-manifest.json' +``` + +### 3) Let's create a simple bot! + +```python +import sys +import logging +import asyncio + +from aiogram.utils.keyboard import InlineKeyboardBuilder + +import config + +from aiogram import Bot, Dispatcher, F +from aiogram.enums import ParseMode +from aiogram.filters import CommandStart, Command +from aiogram.types import Message, CallbackQuery + +from pytonconnect import TonConnect + +logger = logging.getLogger(__file__) + +dp = Dispatcher() +bot = Bot(config.TOKEN, parse_mode=ParseMode.HTML) + +connectors = {} + + +def get_connector(chat_id: int): + if chat_id not in connectors: + connectors[chat_id] = TonConnect(config.MANIFEST_URL) + return connectors[chat_id] + + +@dp.message(CommandStart()) +async def command_start_handler(message: Message): + chat_id = message.chat.id + connector = get_connector(chat_id) + connected = await connector.restore_connection() + if connected: + await message.answer(text='You are already connected!') + else: + wallets_list = TonConnect.get_wallets() + mk_b = InlineKeyboardBuilder() + for wallet in wallets_list: + mk_b.button(text=wallet['name'], callback_data=f'connect:{wallet["name"]}') + mk_b.adjust(1, ) + await message.answer(text='Choose wallet to connect', reply_markup=mk_b.as_markup()) + + +async def main() -> None: + await bot.delete_webhook(drop_pending_updates=True) # skip_updates = True + await dp.start_polling(bot) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, stream=sys.stdout) + asyncio.run(main()) +``` + +In the example above we check if user was connected or not. +And suggest to choose a wallet if user is not connected by providing buttons for all wallets supported by TON Connect. + +### 4) Handler for connection + +```python +from pytoniq_core import Address + +async def connect_wallet(message: Message, wallet_name: str): + connector = get_connector(message.chat.id) + + wallets_list = connector.get_wallets() + wallet = None + + for w in wallets_list: + if w['name'] == wallet_name: + wallet = w + + if wallet is None: + raise Exception('Unknown wallet') + + generated_url = await connector.connect(wallet) + + mk_b = InlineKeyboardBuilder() + mk_b.button(text='Connect', url=generated_url) + + await message.answer(text='Connect wallet within 3 minutes', reply_markup=mk_b.as_markup()) + + for i in range(1, 180): + await asyncio.sleep(1) + if connector.connected: + if connector.account.address: + wallet_address = connector.account.address + wallet_address = Address(wallet_address).to_str(is_bounceable=False) + await message.answer(f'You are connected with address {wallet_address}') + logger.info(f'Connected with address: {wallet_address}') + return + + mk_b = InlineKeyboardBuilder() + mk_b.button(text='Start', callback_data='start') + + await message.answer(f'Timeout error!', reply_markup=mk_b.as_markup()) + + +@dp.callback_query(lambda call: True) +async def main_callback_handler(call: CallbackQuery): + await call.answer() + message = call.message + data = call.data + if data == "start": + await command_start_handler(message) + return + data = data.split(':') + if data[0] == 'connect': + await connect_wallet(message, data[1]) +``` + +Now after user chose wallet, the bot generates link for user to connect and waits 3 minutes until user is connected. + +### 5) Ask for transaction + +Let's take one of the examples from the [Message builders](https://docs.ton.org/develop/dapps/ton-connect/message-builders) article. + +```python +@dp.message(CommandStart()) +async def command_start_handler(message: Message): + chat_id = message.chat.id + connector = get_connector(chat_id) + connected = await connector.restore_connection() + if connected: + await message.answer(text='You are already connected!') + + transaction = { + 'valid_until': int(time.time() + 3600), + 'messages': [ + get_comment_message( + destination_address='0:0000000000000000000000000000000000000000000000000000000000000000', + amount=int(0.01 * 10 ** 9), + comment='hello world!' + ) + ] + } + + await message.answer(text='Approve transaction in your wallet app!') + + await connector.send_transaction( + transaction=transaction + ) +``` + +
+Currently main.py looks like that + +```python +import sys +import logging +import asyncio +import time +from base64 import urlsafe_b64encode + +from aiogram.utils.keyboard import InlineKeyboardBuilder +from pytoniq_core import Address, begin_cell + +import config + +from aiogram import Bot, Dispatcher, F +from aiogram.enums import ParseMode +from aiogram.filters import CommandStart, Command +from aiogram.types import Message, CallbackQuery + +from pytonconnect import TonConnect + +logger = logging.getLogger(__file__) + +dp = Dispatcher() +bot = Bot(config.TOKEN, parse_mode=ParseMode.HTML) + +connectors = {} + + +def get_comment_message(destination_address: str, amount: int, comment: str) -> dict: + + data = { + 'address': destination_address, + 'amount': str(amount), + 'payload': urlsafe_b64encode( + begin_cell() + .store_uint(0, 32) # op code for comment message + .store_string(comment) # store comment + .end_cell() # end cell + .to_boc() # convert it to boc + ) + .decode() # encode it to urlsafe base64 + } + + return data + + +def get_connector(chat_id: int): + if chat_id in connectors: + return connectors[chat_id] + return TonConnect(config.MANIFEST_URL) + + +@dp.message(CommandStart()) +async def command_start_handler(message: Message): + chat_id = message.chat.id + connector = get_connector(chat_id) + connected = await connector.restore_connection() + if connected: + await message.answer(text='You are already connected!') + + transaction = { + 'valid_until': int(time.time() + 3600), + 'messages': [ + get_comment_message( + destination_address='0:0000000000000000000000000000000000000000000000000000000000000000', + amount=int(0.01 * 10 ** 9), + comment='hello world!' + ) + ] + } + + await message.answer(text='Approve transaction in your wallet app!') + + await connector.send_transaction( + transaction=transaction + ) + else: + wallets_list = TonConnect.get_wallets() + mk_b = InlineKeyboardBuilder() + for wallet in wallets_list: + mk_b.button(text=wallet['name'], callback_data=f'connect:{wallet["name"]}') + mk_b.adjust(1, ) + await message.answer(text='Choose wallet to connect', reply_markup=mk_b.as_markup()) + + +async def connect_wallet(message: Message, wallet_name: str): + connector = get_connector(message.chat.id) + + wallets_list = connector.get_wallets() + wallet = None + + for w in wallets_list: + if w['name'] == wallet_name: + wallet = w + + if wallet is None: + raise Exception('Unknown wallet') + + generated_url = await connector.connect(wallet) + + mk_b = InlineKeyboardBuilder() + mk_b.button(text='Connect', url=generated_url) + + await message.answer(text='Connect wallet within 3 minutes', reply_markup=mk_b.as_markup()) + + for i in range(1, 180): + await asyncio.sleep(1) + if connector.connected: + if connector.account.address: + wallet_address = connector.account.address + wallet_address = Address(wallet_address).to_str(is_bounceable=False) + await message.answer(f'You are connected with address {wallet_address}') + logger.info(f'Connected with address: {wallet_address}') + return + + mk_b = InlineKeyboardBuilder() + mk_b.button(text='Start', callback_data='start') + + await message.answer(f'Timeout error!', reply_markup=mk_b.as_markup()) + + +@dp.callback_query(lambda call: True) +async def main_callback_handler(call: CallbackQuery): + await call.answer() + message = call.message + data = call.data + if data == "start": + await command_start_handler(message) + return + data = data.split(':') + if data[0] == 'connect': + await connect_wallet(message, data[1]) + + +async def main() -> None: + await bot.delete_webhook(drop_pending_updates=True) # skip_updates = True + await dp.start_polling(bot) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, stream=sys.stdout) + asyncio.run(main()) + +``` +
+ From 5c4f25a125c45fc0e3f2f344eca0107d19a92c08 Mon Sep 17 00:00:00 2001 From: yungwine Date: Thu, 28 Sep 2023 22:02:57 +0800 Subject: [PATCH 2/4] update sidebars --- sidebars.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sidebars.js b/sidebars.js index 8240fb99b1..378d946a23 100644 --- a/sidebars.js +++ b/sidebars.js @@ -344,7 +344,12 @@ const sidebars = { { type: 'doc', id: 'develop/dapps/ton-connect/tg-bot-integration', - label: 'Telegram Bots', + label: 'Telegram Bots JS', + }, + { + type: 'doc', + id: 'develop/dapps/ton-connect/tg-bot-integration-py', + label: 'Telegram Bots Python', }, ], }, From 92dadfc85525eeafcebb7f20e4d64563fabdc14f Mon Sep 17 00:00:00 2001 From: yungwine Date: Thu, 19 Oct 2023 00:45:00 +0800 Subject: [PATCH 3/4] refactor ton connect python bot article --- .../ton-connect/tg-bot-integration-py.md | 413 ++++++++++++------ 1 file changed, 286 insertions(+), 127 deletions(-) diff --git a/docs/develop/dapps/ton-connect/tg-bot-integration-py.md b/docs/develop/dapps/ton-connect/tg-bot-integration-py.md index a6d5927d5f..be6d0c60c6 100644 --- a/docs/develop/dapps/ton-connect/tg-bot-integration-py.md +++ b/docs/develop/dapps/ton-connect/tg-bot-integration-py.md @@ -3,36 +3,53 @@ In this tutorial, we’ll create a sample telegram bot that supports TON Connect 2.0 authentication using Python TON Connect SDK [pytonconnect](https://github.com/XaBbl4/pytonconnect). We will analyze connecting a wallet, sending a transaction, getting data about the connected wallet, and disconnecting a wallet. -## Implementation +## Preparing -### 1) Installation +### Install libraries -To make bot we are going to use `aiogram` 3.0 Python library. To start integrating TON Connect into your Telegram bot, you need to install the `pytonconnect` package. +To make bot we are going to use `aiogram` 3.0 Python library. +To start integrating TON Connect into your Telegram bot, you need to install the `pytonconnect` package. +And to use TON primitives and parse user address we need `pytoniq-core`. You can use pip for this purpose: ```bash -pip install aiogram -pip install pytonconnect +pip install aiogram pytoniq-core python-dotenv +pip install git+https://github.com/yungwine/pytonconnect.git +``` + +### Set up config +Specify in `.env` file [bot token](https://t.me/BotFather) and link to the TON Connect [manifest file](https://github.com/ton-connect/sdk/tree/main/packages/sdk#add-the-tonconnect-manifest). After load them in `config.py`: + +```dotenv +# .env + +TOKEN='1111111111:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' # your bot token here +MANIFEST_URL='https://raw.githubusercontent.com/XaBbl4/pytonconnect/main/pytonconnect-manifest.json' ``` -### 2) Set up config file -Specify here your [bot token](https://t.me/BotFather), and link to the TON Connect [manifest file](https://github.com/ton-connect/sdk/tree/main/packages/sdk#add-the-tonconnect-manifest). ```python # config.py -TOKEN = '1111111111:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' # your bot token here -MANIFEST_URL = 'https://raw.githubusercontent.com/XaBbl4/pytonconnect/main/pytonconnect-manifest.json' +from os import environ as env + +from dotenv import load_dotenv +load_dotenv() + +TOKEN = env['TOKEN'] +MANIFEST_URL = env['MANIFEST_URL'] ``` -### 3) Let's create a simple bot! +## Create simple bot + +Create `main.py` file which will contain the main bot code: ```python +# main.py + import sys import logging import asyncio -from aiogram.utils.keyboard import InlineKeyboardBuilder - import config from aiogram import Bot, Dispatcher, F @@ -40,55 +57,109 @@ from aiogram.enums import ParseMode from aiogram.filters import CommandStart, Command from aiogram.types import Message, CallbackQuery -from pytonconnect import TonConnect logger = logging.getLogger(__file__) dp = Dispatcher() bot = Bot(config.TOKEN, parse_mode=ParseMode.HTML) + + +@dp.message(CommandStart()) +async def command_start_handler(message: Message): + await message.answer(text='Hi!') + +async def main() -> None: + await bot.delete_webhook(drop_pending_updates=True) # skip_updates = True + await dp.start_polling(bot) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, stream=sys.stdout) + asyncio.run(main()) + +``` + +## Wallet connection + +### TON Connect Storage + +Let's create simple storage for TON Connect + +```python +# tc_storage.py + +from pytonconnect.storage import IStorage, DefaultStorage + + +storage = {} + + +class TcStorage(IStorage): + + def __init__(self, chat_id: int): + self.chat_id = chat_id + + def _get_key(self, key: str): + return str(self.chat_id) + key + + async def set_item(self, key: str, value: str): + storage[self._get_key(key)] = value + + async def get_item(self, key: str, default_value: str = None): + return storage.get(self._get_key(key), default_value) + + async def remove_item(self, key: str): + storage.pop(self._get_key(key)) + +``` + +### Connection handler + +Firstly, we need function which returns different instances for each user: -connectors = {} +```python +# connector.py + +from pytonconnect import TonConnect + +import config +from tc_storage import TcStorage def get_connector(chat_id: int): - if chat_id not in connectors: - connectors[chat_id] = TonConnect(config.MANIFEST_URL) - return connectors[chat_id] - + return TonConnect(config.MANIFEST_URL, storage=TcStorage(chat_id)) + +``` +Secondary, let's add connection handler in `command_start_handler()`: + +```python +# main.py @dp.message(CommandStart()) async def command_start_handler(message: Message): chat_id = message.chat.id connector = get_connector(chat_id) connected = await connector.restore_connection() + + mk_b = InlineKeyboardBuilder() if connected: - await message.answer(text='You are already connected!') + mk_b.button(text='Send Transaction', callback_data='send_tr') + mk_b.button(text='Disconnect', callback_data='disconnect') + await message.answer(text='You are already connected!', reply_markup=mk_b.as_markup()) else: wallets_list = TonConnect.get_wallets() - mk_b = InlineKeyboardBuilder() for wallet in wallets_list: mk_b.button(text=wallet['name'], callback_data=f'connect:{wallet["name"]}') mk_b.adjust(1, ) await message.answer(text='Choose wallet to connect', reply_markup=mk_b.as_markup()) - -async def main() -> None: - await bot.delete_webhook(drop_pending_updates=True) # skip_updates = True - await dp.start_polling(bot) - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO, stream=sys.stdout) - asyncio.run(main()) ``` -In the example above we check if user was connected or not. -And suggest to choose a wallet if user is not connected by providing buttons for all wallets supported by TON Connect. - -### 4) Handler for connection +Now, for a user who has not yet connected a wallet, the bot sends a message with buttons for all available wallets. +So we need to write function to handle `connect:{wallet["name"]}` callbacks: ```python -from pytoniq_core import Address +# main.py async def connect_wallet(message: Message, wallet_name: str): connector = get_connector(message.chat.id) @@ -101,7 +172,7 @@ async def connect_wallet(message: Message, wallet_name: str): wallet = w if wallet is None: - raise Exception('Unknown wallet') + raise Exception(f'Unknown wallet: {wallet_name}') generated_url = await connector.connect(wallet) @@ -110,19 +181,19 @@ async def connect_wallet(message: Message, wallet_name: str): await message.answer(text='Connect wallet within 3 minutes', reply_markup=mk_b.as_markup()) + mk_b = InlineKeyboardBuilder() + mk_b.button(text='Start', callback_data='start') + for i in range(1, 180): await asyncio.sleep(1) if connector.connected: if connector.account.address: wallet_address = connector.account.address wallet_address = Address(wallet_address).to_str(is_bounceable=False) - await message.answer(f'You are connected with address {wallet_address}') + await message.answer(f'You are connected with address {wallet_address}', reply_markup=mk_b.as_markup()) logger.info(f'Connected with address: {wallet_address}') return - mk_b = InlineKeyboardBuilder() - mk_b.button(text='Start', callback_data='start') - await message.answer(f'Timeout error!', reply_markup=mk_b.as_markup()) @@ -133,132 +204,207 @@ async def main_callback_handler(call: CallbackQuery): data = call.data if data == "start": await command_start_handler(message) - return - data = data.split(':') - if data[0] == 'connect': - await connect_wallet(message, data[1]) + elif data == "send_tr": + await send_transaction(message) + elif data == 'disconnect': + await disconnect_wallet(message) + else: + data = data.split(':') + if data[0] == 'connect': + await connect_wallet(message, data[1]) ``` -Now after user chose wallet, the bot generates link for user to connect and waits 3 minutes until user is connected. +Bot gives user 3 minutes to connect a wallet, after which it reports a timeout error. -### 5) Ask for transaction +## Implement Transaction requesting -Let's take one of the examples from the [Message builders](https://docs.ton.org/develop/dapps/ton-connect/message-builders) article. +Let's take one of examples from the [Message builders](https://docs.ton.org/develop/dapps/ton-connect/message-builders) article: ```python -@dp.message(CommandStart()) -async def command_start_handler(message: Message): - chat_id = message.chat.id - connector = get_connector(chat_id) +# messages.py + +from base64 import urlsafe_b64encode + +from pytoniq_core import begin_cell + + +def get_comment_message(destination_address: str, amount: int, comment: str) -> dict: + + data = { + 'address': destination_address, + 'amount': str(amount), + 'payload': urlsafe_b64encode( + begin_cell() + .store_uint(0, 32) # op code for comment message + .store_string(comment) # store comment + .end_cell() # end cell + .to_boc() # convert it to boc + ) + .decode() # encode it to urlsafe base64 + } + + return data + +``` + +And add `send_transaction()` function in the `main.py` file: + +```python +# main.py + +@dp.message(Command('transaction')) +async def send_transaction(message: Message): + connector = get_connector(message.chat.id) connected = await connector.restore_connection() - if connected: - await message.answer(text='You are already connected!') - - transaction = { - 'valid_until': int(time.time() + 3600), - 'messages': [ - get_comment_message( - destination_address='0:0000000000000000000000000000000000000000000000000000000000000000', - amount=int(0.01 * 10 ** 9), - comment='hello world!' - ) - ] - } - - await message.answer(text='Approve transaction in your wallet app!') - - await connector.send_transaction( + if not connected: + await message.answer('Connect wallet first!') + return + + transaction = { + 'valid_until': int(time.time() + 3600), + 'messages': [ + get_comment_message( + destination_address='0:0000000000000000000000000000000000000000000000000000000000000000', + amount=int(0.01 * 10 ** 9), + comment='hello world!' + ) + ] + } + + await message.answer(text='Approve transaction in your wallet app!') + await connector.send_transaction( + transaction=transaction + ) +``` + +But we also should handle possible errors, so we wrap the `send_transaction` method into `try - except` statement: + +```python +@dp.message(Command('transaction')) +async def send_transaction(message: Message): + ... + await message.answer(text='Approve transaction in your wallet app!') + try: + await asyncio.wait_for(connector.send_transaction( transaction=transaction - ) + ), 300) + except asyncio.TimeoutError: + await message.answer(text='Timeout error!') + except pytonconnect.exceptions.UserRejectsError: + await message.answer(text='You rejected the transaction!') + except Exception as e: + await message.answer(text=f'Unknown error: {e}') +``` + +## Add disconnect handler + +This function implementation is simple enough: + +```python +async def disconnect_wallet(message: Message): + connector = get_connector(message.chat.id) + await connector.restore_connection() + await connector.disconnect() + await message.answer('You have been successfully disconnected!') +``` + +Currently, the project has the following structure: + +```bash +. +.env +├── config.py +├── connector.py +├── main.py +├── messages.py +└── tc_storage.py ``` +And the `main.py` looks like this:
-Currently main.py looks like that +Show main.py ```python +# main.py + import sys import logging import asyncio import time -from base64 import urlsafe_b64encode -from aiogram.utils.keyboard import InlineKeyboardBuilder -from pytoniq_core import Address, begin_cell +import pytonconnect.exceptions +from pytoniq_core import Address +from pytonconnect import TonConnect import config +from messages import get_comment_message +from connector import get_connector from aiogram import Bot, Dispatcher, F from aiogram.enums import ParseMode from aiogram.filters import CommandStart, Command from aiogram.types import Message, CallbackQuery +from aiogram.utils.keyboard import InlineKeyboardBuilder -from pytonconnect import TonConnect logger = logging.getLogger(__file__) dp = Dispatcher() bot = Bot(config.TOKEN, parse_mode=ParseMode.HTML) -connectors = {} - - -def get_comment_message(destination_address: str, amount: int, comment: str) -> dict: - - data = { - 'address': destination_address, - 'amount': str(amount), - 'payload': urlsafe_b64encode( - begin_cell() - .store_uint(0, 32) # op code for comment message - .store_string(comment) # store comment - .end_cell() # end cell - .to_boc() # convert it to boc - ) - .decode() # encode it to urlsafe base64 - } - - return data - - -def get_connector(chat_id: int): - if chat_id in connectors: - return connectors[chat_id] - return TonConnect(config.MANIFEST_URL) - @dp.message(CommandStart()) async def command_start_handler(message: Message): chat_id = message.chat.id connector = get_connector(chat_id) connected = await connector.restore_connection() + + mk_b = InlineKeyboardBuilder() if connected: - await message.answer(text='You are already connected!') - - transaction = { - 'valid_until': int(time.time() + 3600), - 'messages': [ - get_comment_message( - destination_address='0:0000000000000000000000000000000000000000000000000000000000000000', - amount=int(0.01 * 10 ** 9), - comment='hello world!' - ) - ] - } - - await message.answer(text='Approve transaction in your wallet app!') - - await connector.send_transaction( - transaction=transaction - ) + mk_b.button(text='Send Transaction', callback_data='send_tr') + mk_b.button(text='Disconnect', callback_data='disconnect') + await message.answer(text='You are already connected!', reply_markup=mk_b.as_markup()) + else: wallets_list = TonConnect.get_wallets() - mk_b = InlineKeyboardBuilder() for wallet in wallets_list: mk_b.button(text=wallet['name'], callback_data=f'connect:{wallet["name"]}') mk_b.adjust(1, ) await message.answer(text='Choose wallet to connect', reply_markup=mk_b.as_markup()) +@dp.message(Command('transaction')) +async def send_transaction(message: Message): + connector = get_connector(message.chat.id) + connected = await connector.restore_connection() + if not connected: + await message.answer('Connect wallet first!') + return + + transaction = { + 'valid_until': int(time.time() + 3600), + 'messages': [ + get_comment_message( + destination_address='0:0000000000000000000000000000000000000000000000000000000000000000', + amount=int(0.01 * 10 ** 9), + comment='hello world!' + ) + ] + } + + await message.answer(text='Approve transaction in your wallet app!') + try: + await asyncio.wait_for(connector.send_transaction( + transaction=transaction + ), 300) + except asyncio.TimeoutError: + await message.answer(text='Timeout error!') + except pytonconnect.exceptions.UserRejectsError: + await message.answer(text='You rejected the transaction!') + except Exception as e: + await message.answer(text=f'Unknown error: {e}') + + async def connect_wallet(message: Message, wallet_name: str): connector = get_connector(message.chat.id) @@ -270,7 +416,7 @@ async def connect_wallet(message: Message, wallet_name: str): wallet = w if wallet is None: - raise Exception('Unknown wallet') + raise Exception(f'Unknown wallet: {wallet_name}') generated_url = await connector.connect(wallet) @@ -279,22 +425,29 @@ async def connect_wallet(message: Message, wallet_name: str): await message.answer(text='Connect wallet within 3 minutes', reply_markup=mk_b.as_markup()) + mk_b = InlineKeyboardBuilder() + mk_b.button(text='Start', callback_data='start') + for i in range(1, 180): await asyncio.sleep(1) if connector.connected: if connector.account.address: wallet_address = connector.account.address wallet_address = Address(wallet_address).to_str(is_bounceable=False) - await message.answer(f'You are connected with address {wallet_address}') + await message.answer(f'You are connected with address {wallet_address}', reply_markup=mk_b.as_markup()) logger.info(f'Connected with address: {wallet_address}') return - mk_b = InlineKeyboardBuilder() - mk_b.button(text='Start', callback_data='start') - await message.answer(f'Timeout error!', reply_markup=mk_b.as_markup()) +async def disconnect_wallet(message: Message): + connector = get_connector(message.chat.id) + await connector.restore_connection() + await connector.disconnect() + await message.answer('You have been successfully disconnected!') + + @dp.callback_query(lambda call: True) async def main_callback_handler(call: CallbackQuery): await call.answer() @@ -302,10 +455,14 @@ async def main_callback_handler(call: CallbackQuery): data = call.data if data == "start": await command_start_handler(message) - return - data = data.split(':') - if data[0] == 'connect': - await connect_wallet(message, data[1]) + elif data == "send_tr": + await send_transaction(message) + elif data == 'disconnect': + await disconnect_wallet(message) + else: + data = data.split(':') + if data[0] == 'connect': + await connect_wallet(message, data[1]) async def main() -> None: @@ -320,3 +477,5 @@ if __name__ == "__main__": ```
+## Improving + From a60bb02e78443a335e683dc8432eb10511a5f096 Mon Sep 17 00:00:00 2001 From: yungwine Date: Tue, 24 Oct 2023 19:34:58 +0800 Subject: [PATCH 4/4] add improve section to python tc bot article --- .../ton-connect/tg-bot-integration-py.md | 86 ++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/docs/develop/dapps/ton-connect/tg-bot-integration-py.md b/docs/develop/dapps/ton-connect/tg-bot-integration-py.md index be6d0c60c6..7a2a64ead2 100644 --- a/docs/develop/dapps/ton-connect/tg-bot-integration-py.md +++ b/docs/develop/dapps/ton-connect/tg-bot-integration-py.md @@ -1,8 +1,13 @@ -# TON Connect for Telegram Bots +import Button from '@site/src/components/button' + +# TON Connect for Telegram Bots - Python In this tutorial, we’ll create a sample telegram bot that supports TON Connect 2.0 authentication using Python TON Connect SDK [pytonconnect](https://github.com/XaBbl4/pytonconnect). We will analyze connecting a wallet, sending a transaction, getting data about the connected wallet, and disconnecting a wallet. + + + ## Preparing ### Install libraries @@ -218,7 +223,7 @@ Bot gives user 3 minutes to connect a wallet, after which it reports a timeout e ## Implement Transaction requesting -Let's take one of examples from the [Message builders](https://docs.ton.org/develop/dapps/ton-connect/message-builders) article: +Let's take one of examples from the [Message builders](/develop/dapps/ton-connect/message-builders) article: ```python # messages.py @@ -479,3 +484,80 @@ if __name__ == "__main__": ## Improving + +### Add permanent storage - Redis + +Currently, our TON Connect Storage uses dict which causes to lost sessions after bot restart. +Let's add permanent database storage with Redis: + +After you launched Redis database install python library to interact with it: + +```bash +pip install redis +``` + +And update `TcStorage` class in `tc_storage.py`: + +```python +import redis.asyncio as redis + +client = redis.Redis(host='localhost', port=6379) + + +class TcStorage(IStorage): + + def __init__(self, chat_id: int): + self.chat_id = chat_id + + def _get_key(self, key: str): + return str(self.chat_id) + key + + async def set_item(self, key: str, value: str): + await client.set(name=self._get_key(key), value=value) + + async def get_item(self, key: str, default_value: str = None): + value = await client.get(name=self._get_key(key)) + return value.decode() if value else default_value + + async def remove_item(self, key: str): + await client.delete(self._get_key(key)) +``` + +### Add QR Code + +Install python `qrcode` package to generate them: + +```bash +pip install qrcode +``` + +Change `connect_wallet()` function so it generates qrcode and sends it as a photo to the user: + +```python +from io import BytesIO +import qrcode +from aiogram.types import BufferedInputFile + + +async def connect_wallet(message: Message, wallet_name: str): + ... + + img = qrcode.make(generated_url) + stream = BytesIO() + img.save(stream) + file = BufferedInputFile(file=stream.getvalue(), filename='qrcode') + + await message.answer_photo(photo=file, caption='Connect wallet within 3 minutes', reply_markup=mk_b.as_markup()) + + ... +``` + +## Summary + +What is next? +- You can add better errors handling in the bot. +- You can add start text and something like `/connect_wallet` command. + +## See Also +- [Full bot code](https://github.com/yungwine/ton-connect-bot) +- [Preparing messages](/develop/dapps/ton-connect/message-builders)