Интеграция смарт-контракта в телеграм бот
Read Time:14 Minute, 6 Second

Интеграция смарт-контракта в телеграм бот

Для 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()

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *