ИНТЕГРАЦИЯ СМАРТ-КОНТРАКТА В ТЕЛЕГРАМ БОТ
Для web3 есть одноименная библиотека написанная для разных языков, в том числе для python, называется web3py.
Здесь мы интегрируем наши 2 проекта в наш телеграм бот. У нас будет игра «Камень, ножницы, бумага» на внутренние токены, смарт-контракт на solidity писали тут и реализуем генерацию ethereum кошельков тык.
Взаиподействие с telegram api переложим на python-telegram-bot. Создадим проект и склонируем репозиторий телеграм бота.
cd ~ && mkdir tgbot && cd tgbot python -m venv .venv source .venv/bin/activate pip install python-telegram-bot coincurve pysha3 web3 git clone https://github.com/python-telegram-bot/python-telegram-bot --recursive cd python-telegram-bot python setup.py install && cd .. && mv -f * ../ && touch start.py
Нам нужно будет хранить немного данных, не будем заморачиваться и используем sqllite, можете создать сами, либо взять просто готувую из гита
CREATE TABLE "users" ( "id" INTEGER NOT NULL UNIQUE, "id_tg" INTEGER NOT NULL UNIQUE, "address" TEXT UNIQUE, "private_key" TEXT UNIQUE, "balance" INTEGER, "win" INTEGER, "lose" INTEGER, "dt_create" TEXT, "first_name" TEXT, "last_name" TEXT, "user_name" TEXT, "last_game" INTEGER, PRIMARY KEY("id" AUTOINCREMENT) )
Так же создадим конфиг и запишем в него кошельки, токен нашел бота и другие параметры
{ "proxy": { "http": "http://login:password@address:port", "https": "http://login:password@address:port" }, "rpc": "https://polygon-rpc.com", "contract": "0x000.... CONTRACT ADDRESS", "bank_wallet_from": "0x000... WALLET", "bank_private_key": "... PRIVATE KEY", "url_addr": "https://polygonscan.com/tx/", "bot_token": "TOKEN TELEGRAM BOT" }
Импортируем нужные библиотеки для работы
import json import logging import secrets import sqlite3 from coincurve import PublicKey from sha3 import keccak_256 from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update, constants from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes from web3 import Web3 from web3.logs import DISCARD
Подключим базу данных с конфигом, создадим наши будущие кнопки для управления
con = sqlite3.connect('db.db') cur = con.cursor() logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) logger = logging.getLogger(__name__) k_1 = [ [ InlineKeyboardButton("Play", callback_data="play_rsp"), InlineKeyboardButton("Balance", callback_data="get_balance"), InlineKeyboardButton("Deposit", callback_data="deposite"), InlineKeyboardButton("Make wallets", callback_data="gen_wal") ] ] k_2 = [ [ InlineKeyboardButton("1", callback_data="1"), InlineKeyboardButton("5", callback_data="5"), InlineKeyboardButton("10", callback_data="10"), InlineKeyboardButton("Return", callback_data="menu") ], ] k_3 = [ [ InlineKeyboardButton(" Rock", callback_data="rock"), InlineKeyboardButton(" Scissors", callback_data="scissors"), InlineKeyboardButton(" Paper", callback_data="paper"), ], [ InlineKeyboardButton("Return", callback_data="menu") ] ] k_4 = [ [ InlineKeyboardButton("Check winner", callback_data="reload") ] ] reply_markup_main = InlineKeyboardMarkup(k_1) reply_markup_numbers = InlineKeyboardMarkup(k_2) reply_markup_play = InlineKeyboardMarkup(k_3) reply_markup_reload = InlineKeyboardMarkup(k_4) with open('./config.json', 'r') as f: config = json.load(f) proxies = config['proxy'] RPC = config['rpc'] contract = config['contract'] bank_wallet_from = config['bank_wallet_from'] bank_private_key = config['bank_private_key'] url_addr = config['url_addr'] BOT_TOKEN = config['bot_token'] ERC20_ABI = json.loads('''[{"inputs": [],"stateMutability": "nonpayable","type": "constructor"},{"anonymous": false,"inputs": [{"indexed": true,"internalType": "address","name": "owner","type": "address"},{"indexed": true,"internalType": "address","name": "spender","type": "address"},{"indexed": false,"internalType": "uint256","name": "value","type": "uint256"}],"name": "Approval","type": "event"},{"anonymous": false,"inputs": [{"indexed": false,"internalType": "uint256","name": "number","type": "uint256"}],"name": "NumberPlay","type": "event"},{"anonymous": false,"inputs": [{"indexed": true,"internalType": "address","name": "from","type": "address"},{"indexed": true,"internalType": "address","name": "to","type": "address"},{"indexed": false,"internalType": "uint256","name": "value","type": "uint256"}],"name": "Transfer","type": "event"},{"inputs": [{"internalType": "address","name": "owner","type": "address"},{"internalType": "address","name": "spender","type": "address"}],"name": "allowance","outputs": [{"internalType": "uint256","name": "","type": "uint256"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "address","name": "spender","type": "address"},{"internalType": "uint256","name": "amount","type": "uint256"}],"name": "approve","outputs": [{"internalType": "bool","name": "","type": "bool"}],"stateMutability": "nonpayable","type": "function"},{"inputs": [{"internalType": "address","name": "account","type": "address"}],"name": "balanceOf","outputs": [{"internalType": "uint256","name": "","type": "uint256"}],"stateMutability": "view","type": "function"},{"inputs": [],"name": "decimals","outputs": [{"internalType": "uint8","name": "","type": "uint8"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "address","name": "spender","type": "address"},{"internalType": "uint256","name": "subtractedValue","type": "uint256"}],"name": "decreaseAllowance","outputs": [{"internalType": "bool","name": "","type": "bool"}],"stateMutability": "nonpayable","type": "function"},{"inputs": [],"name": "gameCost","outputs": [{"internalType": "uint256","name": "","type": "uint256"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "uint256","name": "id","type": "uint256"}],"name": "gameHistory","outputs": [{"components": [{"internalType": "address","name": "player_1","type": "address"},{"internalType": "address","name": "player_2","type": "address"},{"internalType": "int8","name": "player_1_action","type": "int8"},{"internalType": "int8","name": "player_2_action","type": "int8"},{"internalType": "int8","name": "player_win","type": "int8"}],"internalType": "struct RSP._game[]","name": "","type": "tuple[]"}],"stateMutability": "view","type": "function"},{"inputs": [],"name": "gameId","outputs": [{"internalType": "uint256","name": "","type": "uint256"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "address","name": "spender","type": "address"},{"internalType": "uint256","name": "addedValue","type": "uint256"}],"name": "increaseAllowance","outputs": [{"internalType": "bool","name": "","type": "bool"}],"stateMutability": "nonpayable","type": "function"},{"inputs": [],"name": "jackPot","outputs": [{"internalType": "uint256","name": "","type": "uint256"}],"stateMutability": "view","type": "function"},{"inputs": [],"name": "name","outputs": [{"internalType": "string","name": "","type": "string"}],"stateMutability": "view","type": "function"},{"inputs": [],"name": "players","outputs": [{"internalType": "address payable[]","name": "","type": "address[]"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "uint256","name": "_modulus","type": "uint256"}],"name": "randMod","outputs": [{"internalType": "uint256","name": "","type": "uint256"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "uint256","name": "cost","type": "uint256"}],"name": "setGameCost","outputs": [{"internalType": "bool","name": "","type": "bool"}],"stateMutability": "nonpayable","type": "function"},{"inputs": [],"name": "symbol","outputs": [{"internalType": "string","name": "","type": "string"}],"stateMutability": "view","type": "function"},{"inputs": [],"name": "totalSupply","outputs": [{"internalType": "uint256","name": "","type": "uint256"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "address","name": "to","type": "address"},{"internalType": "uint256","name": "amount","type": "uint256"}],"name": "transfer","outputs": [{"internalType": "bool","name": "","type": "bool"}],"stateMutability": "nonpayable","type": "function"},{"inputs": [{"internalType": "address","name": "from","type": "address"},{"internalType": "address","name": "to","type": "address"},{"internalType": "uint256","name": "amount","type": "uint256"}],"name": "transferFrom","outputs": [{"internalType": "bool","name": "","type": "bool"}],"stateMutability": "nonpayable","type": "function"},{"inputs": [{"internalType": "uint256","name": "amount","type": "uint256"},{"internalType": "int8","name": "action","type": "int8"}],"name": "transferPerGame","outputs": [{"internalType": "uint256","name": "","type": "uint256"}],"stateMutability": "nonpayable","type": "function"}]''') w3 = Web3(Web3.HTTPProvider(RPC,request_kwargs={"proxies":proxies})) coin = w3.eth.contract(contract, abi=ERC20_ABI) gasLimit = 210000
Теперь создадим первую функцию, она будет вызываться при первом взаимодействии пользователя с боток, в ней будет механизм регистрации нового пользователя, так же мы там реализуем функции перевода внутренних токенов, а так же небольщой перевод MATIC для оплаты транзакций, т.к. мы используем сеть polygon
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: try: first_name = update.message.chat.first_name last_name = update.message.chat.last_name user_name = update.message.chat.username user_id = update.message.chat.id row = cur.execute("SELECT * FROM users WHERE id_tg = '%s'" % user_id).fetchone() if row: await update.message.reply_text(f"You are already registered!", parse_mode=constants.ParseMode.HTML) await update.message.reply_text("Please choose:", reply_markup=reply_markup_main) return False private_key = keccak_256(secrets.token_bytes(32)).digest() public_key = PublicKey.from_valid_secret(private_key).format(compressed=False)[1:] address = keccak_256(public_key).digest()[-20:] new_address = Web3.toChecksumAddress("0x"+str(address.hex())) await update.message.reply_text(f"Hi Bro\! you are here for the first time, \ so I created a wallet for you, it will be useful for you to interact\. Write \ down your private key and don't share it with anyone\!\n\n\ *Wallet address\:* {new_address}\n\ *Private key\:* ||{private_key.hex()}||", parse_mode=constants.ParseMode.MARKDOWN_V2) await update.message.reply_text(f"And now I will transfer 10 tokens to your wallet so that you can start spending them...\n\n Just wait 5 second...", parse_mode=constants.ParseMode.HTML) transaction = { 'chainId': 137, 'to': new_address, 'from': bank_wallet_from, 'value': Web3.toWei(0.1, 'ether'), 'nonce': w3.eth.getTransactionCount(bank_wallet_from), 'gas': gasLimit, 'gasPrice': w3.eth.gas_price * 2 } signed_txn = w3.eth.account.sign_transaction(transaction, bank_private_key) txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction) native_hash = txn_hash.hex() dict_transaction = { 'chainId': 137, 'from': bank_wallet_from, 'gas': gasLimit, 'gasPrice': w3.eth.gas_price * 2, 'nonce': w3.eth.getTransactionCount(bank_wallet_from) + 1, } coin_decimals = coin.functions.decimals().call() one_coin = 10 * 10 ** coin_decimals transaction = coin.functions.transfer(new_address, one_coin).buildTransaction(dict_transaction) signed_txn = w3.eth.account.sign_transaction(transaction, bank_private_key) txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction) await update.message.reply_text(f"Alright, bro! I sent you <b>10 RSP</b> native tokens and <b>0.1 MATIC</b> for transaction.\n\n\n\ RSP transaction hash: <a href='{url_addr}{txn_hash.hex()}'>{txn_hash.hex()}</a>\n\n\ MATIC transaction hash: <a href='{url_addr}{native_hash}'>{native_hash}</a>", parse_mode=constants.ParseMode.HTML, disable_web_page_preview=True) cur.execute(f"INSERT INTO users (first_name, last_name, user_name, id_tg, address, private_key, balance, win, lose, dt_create) VALUES ('{first_name}','{last_name}','{user_name}','{user_id}' ,'{new_address}', '{private_key.hex()}', 10, 0,0,'2022-05-19');") con.commit() await update.message.reply_text("Please choose:", reply_markup=reply_markup_main) except Exception as e: print(e) await update.message.reply_text("Something wrong... Try again command /start")
Вторая функция будет обрабатывать нажатия наших кнопок, в ней реализованы такие вещи как собственно сама игра с выбором 3 действий, проверка баланса, кран который закинет монет на счет и генерация наших кошелько.
async def button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Parses the CallbackQuery and updates the message text.""" query = update.callback_query user_id = update.callback_query.from_user.id if query.data == "play_rsp": await query.answer() row = cur.execute("SELECT * FROM users WHERE id_tg = '%s'" % user_id).fetchone() if row: cost_play = coin.functions.gameCost().call() / (10**18) reward = cost_play * 2 * .9 if row[4] >= cost_play: await query.edit_message_text(f"The game costs {str(cost_play)} RSP tokens, if you win, you will take {str(reward)} RSP tokens", reply_markup=reply_markup_play) return None else: await query.edit_message_text(f"Sorry, but need more RSP tokens", reply_markup=reply_markup_main) return None elif query.data == "get_balance": row = cur.execute("SELECT * FROM users WHERE id_tg = '%s'" % user_id).fetchone() if row: msg = f"*Your balance:* {row[4]} RSP" else: msg = f"*Your balance:* \-\-\- RSP" elif query.data == "deposite": row = cur.execute("SELECT * FROM users WHERE id_tg = '%s'" % user_id).fetchone() if row: await query.edit_message_text(f"I will transfer 5 tokens to your address\n Just wait 5 second...\n", parse_mode=constants.ParseMode.HTML) dict_transaction = { 'chainId': 137, 'from': bank_wallet_from, 'gas': gasLimit, 'gasPrice': w3.eth.gas_price * 2, 'nonce': w3.eth.getTransactionCount(bank_wallet_from), } coin_decimals = coin.functions.decimals().call() one_coin = 5 * 10 ** coin_decimals transaction = coin.functions.transfer(row[2], one_coin).buildTransaction(dict_transaction) signed_txn = w3.eth.account.sign_transaction(transaction, bank_private_key) txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction) await query.message.reply_text(f"Alright! I sent you 5 RSP. Transaction hash <a href='{url_addr}{txn_hash.hex()}'>{txn_hash.hex()}</a>", parse_mode=constants.ParseMode.HTML) new_balance = row[4] + 5 cur.execute(f"UPDATE users SET balance={new_balance} where id={row[0]};") con.commit() await query.answer() await query.message.reply_text("Please choose:", reply_markup=reply_markup_main) return True elif query.data == "gen_wal": await query.answer() await query.edit_message_text("How many?:", reply_markup=reply_markup_numbers) return None elif query.data == "menu": msg='' elif query.data == "rock": try: await query.answer() await query.edit_message_text(text="Your choice is accepted, the transaction is being executed...") row = cur.execute("SELECT * FROM users WHERE id_tg = '%s'" % user_id).fetchone() if row: cost_play = coin.functions.gameCost().call() dict_transaction = coin.functions.transferPerGame(cost_play,1).buildTransaction({ 'chainId': 137, 'from': row[2], 'gas': gasLimit, 'gasPrice': w3.eth.gas_price * 2, 'nonce': w3.eth.getTransactionCount(row[2]) }) signed_txn = w3.eth.account.signTransaction(dict_transaction, private_key=row[3]) txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction) receipt = w3.eth.wait_for_transaction_receipt(txn_hash,timeout=120, poll_latency=0.1) logs = coin.events.NumberPlay().processReceipt(receipt, errors=DISCARD) number = logs[0]['args']['number'] msg = f"Your playID: {number}\n Push the button and check winner!" except Exception as e: msg = e finally: new_balance = row[4] - (cost_play / 10**18) cur.execute(f"UPDATE users SET balance={new_balance}, last_game={int(number)} where id={row[0]};") con.commit() await query.edit_message_text(msg, reply_markup=reply_markup_reload) return None elif query.data == "scissors": try: await query.answer() await query.edit_message_text(text="Your choice is accepted, the transaction is being executed...") row = cur.execute("SELECT * FROM users WHERE id_tg = '%s'" % user_id).fetchone() if row: cost_play = coin.functions.gameCost().call() dict_transaction = coin.functions.transferPerGame(cost_play,2).buildTransaction({ 'chainId': 137, 'from': row[2], 'gas': gasLimit, 'gasPrice': w3.eth.gas_price * 2, 'nonce': w3.eth.getTransactionCount(row[2]) }) signed_txn = w3.eth.account.signTransaction(dict_transaction, private_key=row[3]) txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction) receipt = w3.eth.wait_for_transaction_receipt(txn_hash,timeout=120, poll_latency=0.1) logs = coin.events.NumberPlay().processReceipt(receipt, errors=DISCARD) number = logs[0]['args']['number'] msg = f"Your playID: {number}\n Push the button and check winner!" except Exception as e: msg = e finally: new_balance = row[4] - (cost_play / 10**18) cur.execute(f"UPDATE users SET balance={new_balance}, last_game={int(number)} where id={row[0]};") con.commit() await query.edit_message_text(msg, reply_markup=reply_markup_reload) return None elif query.data == "paper": try: await query.answer() await query.edit_message_text(text="Your choice is accepted, the transaction is being executed...") row = cur.execute("SELECT * FROM users WHERE id_tg = '%s'" % user_id).fetchone() if row: cost_play = coin.functions.gameCost().call() dict_transaction = coin.functions.transferPerGame(cost_play,3).buildTransaction({ 'chainId': 137, 'from': row[2], 'gas': gasLimit, 'gasPrice': w3.eth.gas_price * 2, 'nonce': w3.eth.getTransactionCount(row[2]) }) signed_txn = w3.eth.account.signTransaction(dict_transaction, private_key=row[3]) txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction) receipt = w3.eth.wait_for_transaction_receipt(txn_hash,timeout=120, poll_latency=0.1) logs = coin.events.NumberPlay().processReceipt(receipt, errors=DISCARD) number = logs[0]['args']['number'] msg = f"Your playID: {number}\n Push the button and check winner!" except Exception as e: msg = e finally: new_balance = row[4] - (cost_play / 10**18) cur.execute(f"UPDATE users SET balance={new_balance}, last_game={int(number)} where id={row[0]};") con.commit() await query.edit_message_text(msg, reply_markup=reply_markup_reload) return None elif query.data == "reload": await query.answer() row = cur.execute("SELECT * FROM users WHERE id_tg = '%s'" % user_id).fetchone() if row: history = coin.functions.gameHistory(row[12]).call() if row[2] == history[0][0]: if history[0][4] == 0: cost_play = coin.functions.gameCost().call() new_balance = row[4] + ((cost_play / 10**18) * 2 / 100 * 90) msg = f"Congratulations!!!\nYou Won!!! Your balance is increased and now equal {new_balance} RSP" cur.execute(f"UPDATE users SET balance={new_balance} where id={row[0]};") elif history[0][4] == 1: msg = "You lost, but next time you will be lucky!!!" else: msg = "No one won in this game, the cost is returned" elif row[2] == history[0][1]: if history[0][4] == 1: cost_play = coin.functions.gameCost().call() new_balance = row[4] + ((cost_play / 10**18) * 2 / 100 * 90) msg = f"Congratulations!!!\nYou Won!!! Your balance is increased and now equal {new_balance} RSP" cur.execute(f"UPDATE users SET balance={new_balance} where id={row[0]};") elif history[0][4] == 0: msg = "You lost, but next time you will be lucky!!!" else: msg = "No one won in this game, the cost is returned" print(history) elif int(query.data) > 0: msg = "<b>ADDRESS:PRIVATE_KEY</b>\n\n" for i in range(0,int(query.data)): private_key = keccak_256(secrets.token_bytes(32)).digest() public_key = PublicKey.from_valid_secret(private_key).format(compressed=False)[1:] address = keccak_256(public_key).digest()[-20:] msg += f"<code>{address.hex()}:{private_key.hex()}</code>\n\n" await query.answer() if msg: await query.edit_message_text(text=msg, parse_mode=constants.ParseMode.HTML) await query.message.reply_text("Please choose:", reply_markup=reply_markup_main) else: await query.edit_message_text("Please choose:", reply_markup=reply_markup_main)
Теперь допишем обработчкики и стартанем наш скрипт
def main() -> None: application = Application.builder().token(BOT_TOKEN).build() application.add_handler(CommandHandler("start", start)) application.add_handler(CallbackQueryHandler(button)) application.run_polling() if __name__ == "__main__": main()