From 7c33bbb408bb23f2a0c0b766e215e7202e4b7b92 Mon Sep 17 00:00:00 2001 From: nyanloutre Date: Mon, 27 Apr 2020 15:32:05 +0200 Subject: [PATCH] migration vers pycoin --- default.nix | 3 +- dogetipbot_telegram.py | 376 +++++++++++++++++++++++++---------------- setup.py | 3 +- 3 files changed, 236 insertions(+), 146 deletions(-) diff --git a/default.nix b/default.nix index 03599c7..e7737f9 100644 --- a/default.nix +++ b/default.nix @@ -16,6 +16,7 @@ python3.pkgs.buildPythonApplication rec { propagatedBuildInputs = with python3.pkgs; [ python-telegram-bot requests - block-io + pycoin + sqlalchemy ]; } diff --git a/dogetipbot_telegram.py b/dogetipbot_telegram.py index d4954f5..928a118 100755 --- a/dogetipbot_telegram.py +++ b/dogetipbot_telegram.py @@ -1,37 +1,102 @@ -from telegram.ext import Updater, CommandHandler -from telegram import ParseMode -from block_io import BlockIo, BlockIoAPIError +from telegram.ext import Updater, CommandHandler, CallbackContext +from telegram import ParseMode, Update +from pycoin.symbols.doge import network +from pycoin.encoding.hexbytes import b2h_rev, h2b, h2b_rev +from sqlalchemy import create_engine, Column, Integer, String +from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.ext.hybrid import hybrid_property import logging import os -import urllib.request +import io +import requests import json import argparse -# BLOCK_IO_API_KEY = os.environ['BLOCK_IO_API_KEY'] -# BLOCK_IO_PIN = os.environ['BLOCK_IO_PIN'] -# TELEGRAM_API_KEY = os.environ['TELEGRAM_API_KEY'] -# NETWORK = os.environ['NETWORK'] - # Parsing arguments -parser = argparse.ArgumentParser(description='Dogetipbot telegram') -parser.add_argument('--block-io-api-key') -parser.add_argument('--block-io-pin') -parser.add_argument('--telegram-api-key') -parser.add_argument('--network') +parser = argparse.ArgumentParser(description="Dogetipbot telegram") +parser.add_argument("--telegram-api-key") +parser.add_argument("--private-key") +parser.add_argument("--db-path") args = parser.parse_args() -BLOCK_IO_API_KEY = args.block_io_api_key -BLOCK_IO_PIN = args.block_io_pin TELEGRAM_API_KEY = args.telegram_api_key -NETWORK = args.network + +# Dogecoin Wallet + +DERIVATION_PATH = "44/3" +TX_FEE_PER_THOUSAND_BYTES = 100000000 +with open(args.private_key, "rb") as f: + seed = f.readline().strip() + DOGE_WALLET = network.keys.bip32_seed(seed) + +# SQL + +engine = create_engine(f"sqlite:///{args.db_path}") +sessionM = sessionmaker(bind=engine) +SESSION = sessionM() +Base = declarative_base() + + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True) + name = Column(String, unique=True) + + @hybrid_property + def private_key(self): + return DOGE_WALLET.subkey_for_path(f"{DERIVATION_PATH}/{self.id}") + + @hybrid_property + def address(self): + return self.private_key.address() + + @hybrid_property + def balance(self): + address_infos = requests.get( + f"https://api.blockcypher.com/v1/doge/main/addrs/{self.address}" + ).json() + return (address_infos["final_balance"], address_infos["unconfirmed_balance"]) + + @hybrid_property + def unspent(self): + unspent_outputs = requests.get( + f"https://api.blockcypher.com/v1/doge/main/addrs/{self.address}?unspentOnly=true&includeScript=true" + ).json() + if unspent_outputs["final_balance"] > 0: + tx_refs = [] + if "txrefs" in unspent_outputs: + tx_refs.extend(unspent_outputs["txrefs"]) + if "unconfirmed_txrefs" in unspent_outputs: + tx_refs.extend(unspent_outputs["unconfirmed_txrefs"]) + tx_ins = [] + for output in tx_refs: + tx_ins.append( + network.Tx.Spendable( + output["value"], + h2b(output["script"]), + h2b_rev(output["tx_hash"]), + output["tx_output_n"], + ) + ) + return tx_ins, unspent_outputs["final_balance"] + + def __repr__(self): + return f"{self.name}: {self.address}" + + +Base.metadata.create_all(engine) # Logging -logging.basicConfig(level=logging.ERROR, - format='%(asctime)s - %(name)s - %(levelname)s - \ - %(message)s') +logging.basicConfig( + level=logging.ERROR, + format="%(asctime)s - %(name)s - %(levelname)s - \ + %(message)s", +) # Exceptions @@ -52,186 +117,209 @@ class NotValidUnit(Exception): pass -# BlockIO - -version = 2 -block_io = BlockIo(BLOCK_IO_API_KEY, BLOCK_IO_PIN, version) - # Core functions -def get_balance(account): - try: - response = block_io.get_address_by(label=account) - except BlockIoAPIError: +def get_user(account): + query = SESSION.query(User).filter_by(name=account) + if query.count() > 0: + return query.first() + else: raise NoAccountError(account) + + +# API broken +# def get_value(amount): +# data = requests.get("https://api.coinmarketcap.com/v1/ticker/dogecoin/?convert=EUR").json() +# return round(float(data[0]['price_eur'])*amount, 2) + + +def transaction(sender, receiver, amount=None): + pkey = get_user(sender).private_key + unspent, balance = get_user(sender).unspent + s = io.BytesIO() + if amount is None: + tx = network.tx_utils.create_signed_tx(unspent, [receiver], wifs=[pkey.wif()]) + tx.stream(s) + tx_byte_count = len(s.getvalue()) + tx = network.tx_utils.create_signed_tx( + unspent, + [receiver], + wifs=[pkey.wif()], + fee=TX_FEE_PER_THOUSAND_BYTES * ((999 + tx_byte_count) // 1000), + ) + elif balance >= amount: + tx = network.tx_utils.create_signed_tx(unspent, [(receiver, amount), pkey.address()], wifs=[pkey.wif()]) + tx.stream(s) + tx_byte_count = len(s.getvalue()) + tx = network.tx_utils.create_signed_tx( + unspent, + [(receiver, amount), pkey.address()], + wifs=[pkey.wif()], + fee=TX_FEE_PER_THOUSAND_BYTES * ((999 + tx_byte_count) // 1000), + ) else: - return (float(response['data']['available_balance']), - float(response['data']['pending_received_balance'])) + raise NotEnoughDoge + push_obj = {"tx": tx.as_hex()} + # print(push_obj) + pushed_tx = requests.post( + "https://api.blockcypher.com/v1/doge/main/txs/push", json=push_obj + ) + pushed_tx.raise_for_status() + return pushed_tx.json()["tx"]["hash"] + # return "null" -def get_value(amount): - if(NETWORK == "DOGE"): - with urllib.request.urlopen("https://api.coinmarketcap.com/v1/ticker" + - "/dogecoin/?convert=EUR") as url: - data = json.loads(url.read().decode()) - return round(float(data[0]['price_eur'])*amount, 2) - - -def create_address(account): - try: - response = block_io.get_new_address(label=account) - except BlockIoAPIError: - raise AccountExisting - else: - return response['data']['address'] - - -def get_address(account): - try: - response = block_io.get_address_by(label=account) - except BlockIoAPIError: - raise NoAccountError(account) - else: - return response['data']['address'] - - -def transaction(sender, receiver, amount): - try: - if get_balance(sender)[0] > amount: - get_address(receiver) - return block_io.withdraw_from_labels(amounts=amount, - from_labels=sender, - to_labels=receiver, - priority="low") - else: - raise NotEnoughDoge - except NoAccountError: - raise - - -def address_transaction(account, address, amount): - try: - if get_balance(account)[0] > amount: - return block_io.withdraw_from_labels(amounts=amount, - from_labels=account, - to_addresses=address, - priority="low") - else: - return NotEnoughDoge - except NoAccountError: - raise - # Telegram functions def start(bot, update): - bot.send_message(chat_id=update.message.chat_id, - text="Bark ! Je suis un tipbot Dogecoin ! \n\n \ - Pour commencer envoyez moi /register") + bot.send_message( + chat_id=update.message.chat_id, + text="Bark ! Je suis un tipbot Dogecoin ! \n\n \ + Pour commencer envoyez moi /register", + ) -def dogetip(bot, update, args): +def dogetip(update: Update, context: CallbackContext): try: - montant = int(args[0]) - unit = args[1] - destinataire = args[2][1:] + montant = int(context.args[0]) + unit = context.args[1] + destinataire = context.args[2][1:] except (IndexError, ValueError): - bot.send_message(chat_id=update.message.chat_id, - text="Syntaxe : /dogetip xxx doge @destinataire") + context.bot.send_message( + chat_id=update.message.chat_id, + text="Syntaxe : /dogetip xxx doge @destinataire", + ) else: try: if unit == "doge": - response = transaction(update.message.from_user.username, - destinataire, montant) + txid = transaction( + update.message.from_user.username, + get_user(destinataire).address, + montant * 100000000, + ) else: raise NotValidUnit(unit) except NotEnoughDoge: message = "Pas assez de doge @" + update.message.from_user.username except NoAccountError as e: - message = "Vous n'avez pas de compte @" + str(e) + '\n\n' \ - + "Utilisez /register pour démarrer" + message = ( + "Vous n'avez pas de compte @" + + str(e) + + "\n\n" + + "Utilisez /register pour démarrer" + ) except NotValidUnit as e: message = str(e) + " n'est pas une unité valide" else: - txid = response['data']['txid'] - message = '🚀 Transaction effectuée 🚀\n\n' \ - + str(montant) + ' ' + NETWORK + '\n' \ - + '@' + update.message.from_user.username + ' → @' \ - + destinataire + '\n\n' \ - + 'Voir la transaction' + message = ( + "🚀 Transaction effectuée 🚀\n\n" + + f"{str(montant)} DOGE\n" + + f"@{update.message.from_user.username} → @{destinataire}\n\n" + + f'Voir la transaction' + ) - bot.send_message(chat_id=update.message.chat_id, - parse_mode=ParseMode.HTML, text=message) + context.bot.send_message( + chat_id=update.message.chat_id, parse_mode=ParseMode.HTML, text=message + ) -def register(bot, update): - try: - address = create_address(update.message.from_user.username) - except AccountExisting: - bot.send_message(chat_id=update.message.chat_id, - text="Vous avez déjà un compte") +def register(update: Update, context: CallbackContext): + query = SESSION.query(User).filter_by(name=update.message.from_user.username) + if query.count() > 0: + update.message.reply_text("Vous avez déjà un compte") else: - bot.send_message(chat_id=update.message.chat_id, text=address) + user = User(name=update.message.from_user.username) + SESSION.add(user) + SESSION.commit() + update.message.reply_text(get_user(update.message.from_user.username).address) -def infos(bot, update): +def infos(update: Update, context: CallbackContext): try: - address = get_address(update.message.from_user.username) - balance, unconfirmed_balance = \ - get_balance(update.message.from_user.username) - value = get_value(balance) + address = get_user(update.message.from_user.username).address + balance, unconfirmed_balance = get_user( + update.message.from_user.username + ).balance except NoAccountError as e: - bot.send_message(chat_id=update.message.chat_id, - text="Vous n'avez pas de compte @" + str(e) + '\n\n' - + "Utilisez /register pour démarrer") + update.message.reply_text( + "Vous n'avez pas de compte @" + + str(e) + + "\n\n" + + "Utilisez /register pour démarrer" + ) else: - bot.send_message(chat_id=update.message.chat_id, - text=address + "\n\n" + - str(balance) + " " + NETWORK + - " (" + str(value) + " €)" + "\n" + - str(unconfirmed_balance) + " " + - NETWORK + " unconfirmed") + update.message.reply_text( + address + + "\n\n" + + str(balance / 100000000) + + " DOGE\n" + + str(unconfirmed_balance / 100000000) + + " DOGE unconfirmed" + ) -def withdraw(bot, update, args): - montant = int(args[0]) - unit = args[1] - address = args[2] +def withdraw(update: Update, context: CallbackContext): + try: + unit = context.args[1] + address = context.args[2] + except (IndexError, ValueError): + context.bot.send_message( + chat_id=update.message.chat_id, text="Syntaxe : /withdraw xxx doge adresse" + ) + else: + if unit == "doge": + if context.args[0] == "all": + txid = transaction(update.message.from_user.username, address) + else: + montant = int(context.args[0]) + txid = transaction( + update.message.from_user.username, address, montant * 100000000 + ) - if unit == "doge": - response = address_transaction(update.message.from_user.username, - address, montant) + context.bot.send_message( + chat_id=update.message.chat_id, + parse_mode=ParseMode.MARKDOWN, + text="Transaction effectuée !\n" + + f"[tx](https://blockchair.com/dogecoin/transaction/{txid})", + ) - txid = response['data']['txid'] - bot.send_message(chat_id=update.message.chat_id, - parse_mode=ParseMode.MARKDOWN, - text="Transaction effectuée !\n" + - "[tx](https://chain.so/tx/" + NETWORK + "/" + txid + ")") +def users(update: Update, context: CallbackContext): + query = SESSION.query(User).all() + reply = "" + for user in query: + reply += ( + f"\n{repr(user)}\thttps://blockchair.com/dogecoin/address/{user.address}" + ) + update.message.reply_text(reply) # Telegram initialisation -updater = Updater(token=TELEGRAM_API_KEY) +updater = Updater(token=TELEGRAM_API_KEY, use_context=True) dispatcher = updater.dispatcher -start_handler = CommandHandler('start', start) +start_handler = CommandHandler("start", start) dispatcher.add_handler(start_handler) -dogetip_handler = CommandHandler('dogetip', dogetip, pass_args=True) +dogetip_handler = CommandHandler("dogetip", dogetip) dispatcher.add_handler(dogetip_handler) -register_handler = CommandHandler('register', register) +register_handler = CommandHandler("register", register) dispatcher.add_handler(register_handler) -infos_handler = CommandHandler('infos', infos) +infos_handler = CommandHandler("infos", infos) dispatcher.add_handler(infos_handler) -withdraw_handler = CommandHandler('withdraw', withdraw, pass_args=True) +infos_handler = CommandHandler("users", users) +dispatcher.add_handler(infos_handler) + +withdraw_handler = CommandHandler("withdraw", withdraw) dispatcher.add_handler(withdraw_handler) + def main(): updater.start_polling() updater.idle() diff --git a/setup.py b/setup.py index dab33d1..80cc86f 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ setup( install_requires=[ 'python-telegram-bot', 'requests', - 'block-io' + 'pycoin', + 'sqlalchemy' ] )