1
Fork 0
mirror of https://github.com/Steffo99/greed.git synced 2025-03-13 12:17:27 +00:00

complete bitcoin integration

This commit is contained in:
Darren 2020-01-13 14:47:25 +02:00
parent 83bf6acec6
commit 4ee78c4e87
7 changed files with 306 additions and 60 deletions

54
blockonomics.py Normal file
View file

@ -0,0 +1,54 @@
import configloader
import requests
# Define all the database tables using the sqlalchemy declarative base
class Blockonomics:
def fetch_new_btc_price():
url = 'https://www.blockonomics.co/api/price'
params = {'currency':configloader.config["Payments"]["currency"]}
r = requests.get(url,params)
if r.status_code == 200:
price = r.json()['price']
print ('Bitcoin price ' + str(price))
return price
else:
print(r.status_code, r.text)
def new_address(reset=False):
api_key = configloader.config["Bitcoin"]["api_key"]
secret = configloader.config["Bitcoin"]["secret"]
url = 'https://www.blockonomics.co/api/new_address'
if reset == True:
url += '?match_callback='+secret+'&reset=1'
else:
url += "?match_callback=" + secret
headers = {'Authorization': "Bearer " + api_key}
print(url)
r = requests.post(url, headers=headers)
if r.status_code == 200:
return r
else:
print(r.status_code, r.text)
return r
def get_callbacks():
api_key = configloader.config["Bitcoin"]["api_key"]
url = 'https://www.blockonomics.co/api/address?&no_balance=true&only_xpub=true&get_callback=true'
headers = {'Authorization': "Bearer " + api_key}
r = requests.get(url, headers=headers)
if r.status_code == 200:
return r
else:
return False
def update_callback(callback_url, xpub):
api_key = configloader.config["Bitcoin"]["api_key"]
url = 'https://www.blockonomics.co/api/update_callback'
headers = {'Authorization': "Bearer " + api_key}
data = {'callback':callback_url, 'xpub':xpub}
print(data)
r = requests.post(url, headers=headers, json = data)
if r.status_code == 200:
return r
else:
print(r.status_code, r.text)

View file

@ -63,6 +63,7 @@ phone_required = yes
[Bitcoin]
; Blockonomics API key
api_key = YOUR_API_HERE_
secret = YOUR_SECRET_HERE_
# Bot appearance settings
[Appearance]

134
core.py
View file

@ -5,7 +5,10 @@ import worker
import configloader
import utils
import threading
import flask
from blockonomics import Blockonomics
import database as db
import datetime
def main():
"""The core code of the program. Should be run only in the main process!"""
@ -117,7 +120,134 @@ def main():
# Mark them as read by increasing the update_offset
next_update = updates[-1].update_id + 1
# Start the bitcoin callback listener
app = flask.Flask(__name__)
@app.route('/callback', methods=['GET'])
def callback():
network_confirmations = 2
# Fetch the callback parameters
secret = flask.request.args.get("secret")
status = int(flask.request.args.get("status"))
address = flask.request.args.get("addr")
# Check the status and secret
if secret == configloader.config["Bitcoin"]["secret"]:
# Fetch the current transaction by address
dbsession = db.Session()
transaction = dbsession.query(db.BtcTransaction).filter(db.BtcTransaction.address == address).one_or_none()
if status >= network_confirmations:
if transaction.txid == "":
# Check timestamp
# If timeout period has past update and use new btc price
current_time = datetime.datetime.now()
timeout = 30
print (transaction)
if current_time - datetime.timedelta(minutes = timeout) > datetime.datetime.strptime(transaction.timestamp, '%Y-%m-%d %H:%M:%S.%f'):
transaction.price = Blockonomics.fetch_new_btc_price()
# Convert satoshi to fiat
satoshi = float(flask.request.args.get("value"))
received_btc = satoshi/1.0e8
received_value = received_btc*transaction.price
print ("Recieved "+str(received_value)+" "+configloader.config["Payments"]["currency"]+" on address "+address)
# Add the credit to the user account
user = dbsession.query(db.User).filter(db.User.user_id == transaction.user_id).one_or_none()
user.credit += received_value
# Update the value + txid + status + timestamp for transaction in DB
transaction.value += received_value
transaction.txid = flask.request.args.get("txid")
transaction.status = status
transaction.timestamp = current_time
print (transaction.value)
# Add a transaction to list
new_transaction = db.Transaction(user=user,
value=received_value,
provider="Bitcoin",
notes = address)
# Add and commit the transaction
dbsession.add(new_transaction)
dbsession.commit()
return "Success"
else:
return "Transaction already proccessed"
else:
return "Not enough confirmations"
else:
return "Incorrect secret"
@app.route('/test_setup', methods=['GET', 'POST'])
def test_setup():
response = Blockonomics.get_callbacks()
error_str = ''
print (response.json()[0]['address'])
response_body = response.json()
if response_body[0]:
response_callback = response_body[0]['callback']
response_address = response_body[0]['address']
else :
response_callback = ''
response_address = ''
print (response_address)
callback_secret = configloader.config["Bitcoin"]["secret"]
api_url = flask.url_for('callback', _external=True)
callback_url = api_url + "?secret=" + callback_secret
return_string = "Blockonomics Callback URL: " + callback_url
# Remove http:# or https:# from urls
api_url_without_schema = api_url.replace("https://","")
api_url_without_schema = api_url_without_schema.replace("http://","")
callback_url_without_schema = callback_url.replace("https://","")
callback_url_without_schema = callback_url_without_schema.replace("http://","")
response_callback_without_schema = response_callback.replace("https://","")
response_callback_without_schema = response_callback_without_schema.replace("http://","")
# TODO: Check This: WE should actually check code for timeout
if response == False:
error_str = 'Your server is blocking outgoing HTTPS calls'
elif response.status_code==401:
error_str = 'API Key is incorrect'
elif response.status_code!=200:
error_str = response.text
elif response_body == None or len(response_body) == 0:
error_str = 'You have not entered an xpub'
elif len(response_body) == 1:
print ("response callback" + response_callback)
if response_callback == "":
print ("No callback URL set, set one")
# No callback URL set, set one
Blockonomics.update_callback(callback_url, response_address)
elif response_callback_without_schema != callback_url_without_schema:
print ("callback URL set, checking secret")
base_url = api_url_without_schema
# Check if only secret differs
if base_url in response_callback:
# Looks like the user regenrated callback by mistake
# Just force Update_callback on server
Blockonomics.update_callback(callback_url, response_address)
else:
error_str = "You have an existing callback URL. Refer instructions on integrating multiple websites"
else:
error_str = "You have an existing callback URL. Refer instructions on integrating multiple websites"
# Check if callback url is set
for res_obj in response_body:
res_url = res_obj['callback'].replace("https://","")
res_url = res_url.replace("https://","")
if res_url == callback_url_without_schema:
error_str = ""
if error_str == "":
# Everything OK ! Test address generation
response = Blockonomics.new_address(True)
if response.status_code != 200:
error_str = response.text
if error_str:
error_str = error_str + '<p>For more information, please consult <a href="https://blockonomics.freshdesk.com/support/solutions/articles/33000215104-troubleshooting-unable-to-generate-new-address" target="_blank">this troubleshooting article</a></p>'
return error_str + '<br>' + return_string
# No errors
return 'Congrats ! Setup is all done<br>' + return_string
def flaskThread():
app.run(threaded=True)
# Run the main function only in the main process
if __name__ == "__main__":
main()
# Start callback in thread
threading.Thread(target=flaskThread).start()
main()

View file

@ -1,6 +1,6 @@
import typing
from sqlalchemy import create_engine, Column, ForeignKey, UniqueConstraint
from sqlalchemy import Integer, BigInteger, String, Text, LargeBinary, DateTime, Boolean
from sqlalchemy import Integer, BigInteger, String, Text, LargeBinary, DateTime, Boolean, Float
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.ext.declarative import declarative_base
import configloader
@ -186,6 +186,40 @@ class Transaction(TableDeclarativeBase):
def __repr__(self):
return f"<Transaction {self.transaction_id} for User {self.user_id} {str(self)}>"
class BtcTransaction(TableDeclarativeBase):
"""A greed wallet transaction.
Wallet credit ISN'T calculated from these, but they can be used to recalculate it."""
# TODO: split this into multiple tables
# The internal transaction ID
transaction_id = Column(Integer, primary_key=True)
# The user whose credit is affected by this transaction
user_id = Column(BigInteger, ForeignKey("users.user_id"), nullable=False)
user = relationship("User")
# The value of this transaction. Can be both negative and positive.
price = Column(Float)
value = Column(Float)
satoshi = Column(Integer, nullable=False)
currency = Column(Text)
status = Column(Integer, nullable=False)
timestamp = Column(Integer)
# Extra notes on the transaction
address = Column(Text)
txid = Column(Text)
# Extra table parameters
__tablename__ = "btc_transactions"
def __str__(self):
string = f"<b>T{self.transaction_id}</b> | {str(self.user)} | {str(self.price)} | {str(self.value)} | {str(self.currency)} | {str(self.status)} | {str(self.timestamp)} | {str(self.address)}"
if self.satoshi:
string += f" | {self.satoshi}"
if self.txid:
string += f" | {self.txid}"
return string
def __repr__(self):
return f"<Transaction {self.transaction_id} for User {self.user_id} {str(self)}>"
class Admin(TableDeclarativeBase):
"""A greed administrator with his permissions."""

View file

@ -145,6 +145,9 @@ menu_cash = "💵 In contanti"
# User menu: credit card
menu_credit_card = "💳 Con una carta di credito"
# User menu: bitcoin
menu_bitcoin = "🛡 Bitcoin"
# Admin menu: products
menu_products = "📝️ Prodotti"

View file

@ -217,6 +217,11 @@ class DuckBot:
def send_document(self, *args, **kwargs):
return self.bot.send_document(*args, **kwargs)
@catch_telegram_errors
def send_message_markdown(self, *args, **kwargs):
# All messages are sent in HTML parse mode
return self.bot.send_message(parse_mode="Markdown", *args, **kwargs)
# More methods can be added here

133
worker.py
View file

@ -13,8 +13,10 @@ import utils
import os
from html import escape
import requests
from blockonomics import Blockonomics
from websocket import create_connection
import time
import queue
class StopSignal:
"""A data class that should be sent to the worker when the conversation has to be stopped abnormally."""
@ -46,10 +48,6 @@ class ChatWorker(threading.Thread):
self.queue = queuem.Queue()
# The current active invoice payload; reject all invoices with a different payload
self.invoice_payload = None
# The current btc amount for the chat
self.btc_price = 0
# The current btc address for the chat
self.btc_address = ""
# The Sentry client for reporting errors encountered by the user
if configloader.config["Error Reporting"]["sentry_token"] != \
"https://00000000000000000000000000000000:00000000000000000000000000000000@sentry.io/0000000":
@ -213,29 +211,6 @@ class ChatWorker(threading.Thread):
# Return the precheckoutquery
return update.pre_checkout_query
def __fetch_new_btc_price(self):
url = 'https://www.blockonomics.co/api/price'
params = {'currency':configloader.config["Payments"]["currency"]}
r = requests.get(url,params)
if r.status_code == 200:
price = r.json()['price']
print ('Bitcoin price ' + str(price))
self.btc_price = price
else:
print(r.status_code, r.text)
def __fetch_new_btc_address(self):
api_key = configloader.config["Bitcoin"]["api_key"]
url = 'https://www.blockonomics.co/api/new_address'
headers = {'Authorization': "Bearer " + api_key}
r = requests.post(url, headers=headers)
if r.status_code == 200:
address = r.json()['address']
print ('Payment receiving address ' + address)
self.btc_address = address
else:
print(r.status_code, r.text)
def __wait_for_successfulpayment(self) -> telegram.SuccessfulPayment:
"""Continue getting updates until a successfulpayment is received."""
while True:
@ -251,14 +226,46 @@ class ChatWorker(threading.Thread):
return update.message.successful_payment
def __wait_for_successfulbtcpayment(self, address):
while True:
timestamp = datetime.datetime.now()
ws = create_connection("wss://www.blockonomics.co/payment/" + address)
print("Connected to websocket...")
# Start the websocket
ws = create_connection("wss://www.blockonomics.co/payment/" + address)
print("Connected to websocket...")
response = ""
def cancel_thread(stop_event):
while not stop_event.is_set():
# Wait for inline keyboard
stuff_complete = self.__wait_for_inlinekeyboard_callback()
# some check if stuff is complete
if stuff_complete:
response = "Cancelled"
stop_event.set()
break
def __wait_for_websocket(stop_event):
result = ws.recv()
print("Received '%s'" % result)
ws.close()
return result
response = "Received"
# wait for 2 seconds for callback
time.sleep(2)
stop_event.set()
def run_threads():
# create a thread event
a_stop_event = threading.Event()
# start the cancel thread
t = threading.Thread(target=cancel_thread, args=[a_stop_event])
t.start()
# start the recieving thread
t = threading.Thread(target=__wait_for_websocket, args=[a_stop_event])
t.start()
# wait for an event
while not a_stop_event.is_set():
time.sleep(0.1)
print ("At least one thread is done")
run_threads()
ws.close()
return response
def __wait_for_photo(self, cancellable: bool=False) -> typing.Union[typing.List[telegram.PhotoSize], CancelSignal]:
"""Continue getting updates until a photo is received, then return it."""
@ -329,6 +336,8 @@ class ChatWorker(threading.Thread):
Normal bot actions should be placed here."""
# Loop used to returning to the menu after executing a command
while True:
# Before the user reply, update the user data
self.update_user()
# Create a keyboard with the user main menu
keyboard = [[telegram.KeyboardButton(strings.menu_order)],
[telegram.KeyboardButton(strings.menu_order_status)],
@ -751,31 +760,41 @@ class ChatWorker(threading.Thread):
[telegram.InlineKeyboardButton(strings.menu_cancel,
callback_data="cmd_cancel")]])
# The amount is valid, fetch btc amount and address
self.__fetch_new_btc_price()
satoshi_amount = int(1.0e8*float(raw_value)/float(self.btc_price))
btc_price = Blockonomics.fetch_new_btc_price()
satoshi_amount = int(1.0e8*float(raw_value)/float(btc_price))
btc_amount = satoshi_amount/1.0e8
self.__fetch_new_btc_address()
self.bot.send_message(self.chat.id, "To pay, send this amount:\n" + str(btc_amount) +
"\nto this bitcoin address:\n" + self.btc_address)
# Should check to re-use addresses in future
btc_address = Blockonomics.new_address().json()["address"]
# Create a new database btc transaction
transaction = db.BtcTransaction(user=self.user,
price = btc_price,
value=0,
satoshi = satoshi_amount,
currency = configloader.config["Payments"]["currency"],
status = -1,
timestamp = datetime.datetime.now(),
address=btc_address,
txid='')
#Add and commit the btc transaction
self.session.add(transaction)
self.session.commit()
#inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton("Open In Wallet",url="https://google.com")]])
# Create the keyboard with the cancel button
inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_cancel,
callback_data="cart_cancel")]])
# Send a message containing the button to cancel or pay
self.bot.send_message_markdown(self.chat.id, "To pay, send this amount:\n`"
+ str(btc_amount)
+ "`\nto this bitcoin address:\n`"
+ btc_address + "`", reply_markup=inline_keyboard)
# Wait for the bitcoin payment
# asyncio.get_event_loop().run_until_complete(self.__wait_for_successfulbtcpayment())
successfulpayment = self.__wait_for_successfulbtcpayment(self.btc_address)
self.bot.send_message(self.chat.id, "Payment recieved!\nYour account will be credited on confirmation.")
# Create a new database transaction
# transaction = db.Transaction(user=self.user,
# value=successfulpayment.total_amount,
# provider="Bitcoin",
# telegram_charge_id="",
# provider_charge_id="")
# if successfulpayment.order_info is not None:
# transaction.payment_name = successfulpayment.order_info.name
# transaction.payment_email = successfulpayment.order_info.email
# transaction.payment_phone = successfulpayment.order_info.phone_number
# Add the credit to the user account
# self.user.credit += successfulpayment.total_amount
# Add and commit the transaction
# self.session.add(transaction)
# self.session.commit()
successfulpayment = self.__wait_for_successfulbtcpayment(btc_address)
if successfulpayment == "Received":
self.bot.send_message(self.chat.id, "Payment recieved!\nYour account will be credited on confirmation.")
else:
self.bot.send_message(self.chat.id, "Payment cancelled")
# successfulpayment = {"status": 0, "timestamp": 1578567378, "value": "100000", "txid": "WarningThisIsAGeneratedTestPaymentAndNotARealBitcoinTransaction"}
# self.bot.send_message(self.chat.id, "Payment recieved!\nYour account will be credited on confirmation.")
def __bot_info(self):
"""Send information about the bot."""