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'
]
)