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:
parent
83bf6acec6
commit
4ee78c4e87
7 changed files with 306 additions and 60 deletions
54
blockonomics.py
Normal file
54
blockonomics.py
Normal 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)
|
|
@ -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
134
core.py
|
@ -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()
|
36
database.py
36
database.py
|
@ -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."""
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
5
utils.py
5
utils.py
|
@ -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
133
worker.py
|
@ -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."""
|
||||
|
|
Loading…
Add table
Reference in a new issue