Made the user_publickeys to be read from file. Black is now used for formating.

This commit is contained in:
kalzu 2023-01-01 20:04:47 +02:00
parent cb33802e87
commit 21f0d0c229
2 changed files with 327 additions and 287 deletions

View File

@ -1,11 +1,11 @@
#!/usr/bin/python3 #!/usr/bin/python3
''' """
Fetch BTCUSD OHLC data from few market places and serve it forward with simple json api. Fetch BTCUSD OHLC data from few market places and serve it forward with simple json api.
Creates: ./btc_ohlc.db Creates: ./btc_ohlc.db
serves: localhost:5000/[t] and /serverkey serves: localhost:5000/[t] and /serverkey
Authentication via auth header with signatures Authentication via auth header with signatures
''' """
import math import math
import json import json
@ -19,30 +19,43 @@ import requests
import ecdsa import ecdsa
import krakenex import krakenex
from flask import Flask, jsonify, request from flask import Flask, jsonify, request
# from Cryptodome.Cipher import AES # from Cryptodome.Cipher import AES
DATABASE = "btc_ohlc.db" DATABASE = "btc_ohlc.db"
KEYSFILE = "userkeys.json"
app = Flask(__name__) app = Flask(__name__)
## Add your public key here
user_publickeys = {
"user1": 'f1debc13fb21fe0eee54525aa4f8aae5733b201c755edaa55f8893c90aa375b261a62eaa3110651ac5d7705d402581256a37508b0a1ca28bd919ea44710d9c88'
}
## Generate the ECDSA keys for this instance ## Generate the ECDSA keys for this instance
print("Generating ECDSA keys for this instance... just wait a bit...") print("Generating ECDSA keys for this instance... just wait a bit...")
server_private_key = ecdsa.SigningKey.generate(curve=ecdsa.SECP256k1) server_private_key = ecdsa.SigningKey.generate(curve=ecdsa.SECP256k1)
server_public_key = server_private_key.get_verifying_key() server_public_key = server_private_key.get_verifying_key()
# We need the hexadecimal form for sharing over http/json # We need the hexadecimal form for sharing over http/json
server_public_key_hex = binascii.hexlify(server_public_key.to_string()).decode('utf-8') server_public_key_hex = binascii.hexlify(server_public_key.to_string()).decode("utf-8")
database_lock = threading.Lock() database_lock = threading.Lock()
# Empty response json # Empty response json
empty_dict = {"exchange": "", "timestamp": 0, "open": 0, "high": 0, "low": 0, "close": 0, "volume_quote": 0, "volume_base": 0, "trades": 0} empty_dict = {
"exchange": "",
"timestamp": 0,
"open": 0,
"high": 0,
"low": 0,
"close": 0,
"volume_quote": 0,
"volume_base": 0,
"trades": 0,
}
empty_json = json.dumps(empty_dict) empty_json = json.dumps(empty_dict)
def read_keys():
with open(KEYSFILE, "r") as cfile:
user_keys = json.load(cfile)
return user_keys["user_publickeys"]
def check_database(): def check_database():
""" """
Check the database for the 'ohlc' table. Check the database for the 'ohlc' table.
@ -50,7 +63,8 @@ def check_database():
""" """
if not os.path.exists(DATABASE): if not os.path.exists(DATABASE):
new_db = sqlite3.connect(DATABASE) new_db = sqlite3.connect(DATABASE)
new_db.execute("""\ new_db.execute(
"""\
CREATE TABLE ohlc ( CREATE TABLE ohlc (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
exchange TEXT NOT NULL, exchange TEXT NOT NULL,
@ -61,7 +75,8 @@ def check_database():
close REAL NOT NULL, close REAL NOT NULL,
volume_quote REAL NOT NULL, volume_quote REAL NOT NULL,
volume_base REAL NOT NULL, volume_base REAL NOT NULL,
trades INTEGER NOT NULL )""") trades INTEGER NOT NULL )"""
)
new_db.commit() new_db.commit()
new_db.close() new_db.close()
@ -76,7 +91,8 @@ def check_database():
# Create the table if it doesn't exist # Create the table if it doesn't exist
if not table_exists: if not table_exists:
new_db.execute("""\ new_db.execute(
"""\
CREATE TABLE ohlc ( CREATE TABLE ohlc (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
exchange TEXT NOT NULL, exchange TEXT NOT NULL,
@ -87,9 +103,11 @@ def check_database():
close REAL NOT NULL, close REAL NOT NULL,
volume_quote REAL NOT NULL, volume_quote REAL NOT NULL,
volume_base REAL NOT NULL, volume_base REAL NOT NULL,
trades INTEGER NOT NULL )""") trades INTEGER NOT NULL )"""
)
new_db.commit() new_db.commit()
def fetch_kraken(): def fetch_kraken():
""" """
Fetch BTCUSD OHLC data from Kraken in json. Fetch BTCUSD OHLC data from Kraken in json.
@ -98,88 +116,89 @@ def fetch_kraken():
""" """
kraken = krakenex.API() kraken = krakenex.API()
response = kraken.query_public('OHLC', {'pair': 'BTCUSD', 'interval': 240 }) response = kraken.query_public("OHLC", {"pair": "BTCUSD", "interval": 240})
ohlc_data = response['result']['XXBTZUSD'] ohlc_data = response["result"]["XXBTZUSD"]
candle_stick_data = { candle_stick_data = {
'exchange': 'kraken', "exchange": "kraken",
'timestamp': ohlc_data[1][0], "timestamp": ohlc_data[1][0],
'open': ohlc_data[0][1], "open": ohlc_data[0][1],
'high': max(item[2] for item in ohlc_data), "high": max(item[2] for item in ohlc_data),
'low': min(item[3] for item in ohlc_data), "low": min(item[3] for item in ohlc_data),
'close': ohlc_data[-1][4], "close": ohlc_data[-1][4],
'volume_quote': sum(float(item[5]) for item in ohlc_data), "volume_quote": sum(float(item[5]) for item in ohlc_data),
'volume_base': sum(float(item[6]) for item in ohlc_data), "volume_base": sum(float(item[6]) for item in ohlc_data),
'trades': sum(item[7] for item in ohlc_data), "trades": sum(item[7] for item in ohlc_data),
} }
kraken_json = json.dumps(candle_stick_data, indent=2) kraken_json = json.dumps(candle_stick_data, indent=2)
#print("Kraken: OK")
#print(kraken_json)
return kraken_json return kraken_json
def fetch_bitstamp(): def fetch_bitstamp():
""" """
Fetch Bitstamp data ja serve it as json. Fetch Bitstamp data ja serve it as json.
Returns: Returns:
str: 5min OHLC data in JSON format. str: 5min OHLC data in JSON format.
""" """
response = requests.get("https://www.bitstamp.net/api/v2/ohlc/btcusd/?step=300&limit=1") response = requests.get(
"https://www.bitstamp.net/api/v2/ohlc/btcusd/?step=300&limit=1"
)
if response.status_code == 200: # check if the request was successful if response.status_code == 200: # check if the request was successful
bitstamp_data = response.json() bitstamp_data = response.json()
ohlc_data = bitstamp_data["data"]["ohlc"] ohlc_data = bitstamp_data["data"]["ohlc"]
candle_stick_data = { candle_stick_data = {
'exchange': 'bitstamp', "exchange": "bitstamp",
'timestamp': int(ohlc_data[0]['timestamp']), "timestamp": int(ohlc_data[0]["timestamp"]),
'open': float(ohlc_data[0]['open']), "open": float(ohlc_data[0]["open"]),
'high': float(ohlc_data[0]['high']), "high": float(ohlc_data[0]["high"]),
'low': float(ohlc_data[0]['low']), "low": float(ohlc_data[0]["low"]),
'close': float(ohlc_data[0]['close']), "close": float(ohlc_data[0]["close"]),
'volume_quote': float(ohlc_data[0]['volume']), "volume_quote": float(ohlc_data[0]["volume"]),
'volume_base': 0, # not provided by Bitstamp API "volume_base": 0, # not provided by Bitstamp API
'trades': 0, # not provided by Bitstamp API "trades": 0, # not provided by Bitstamp API
} }
bitstamp_json = json.dumps(candle_stick_data, indent=2) bitstamp_json = json.dumps(candle_stick_data, indent=2)
#print("Bitstamp: OK")
# print(bitstamp_json)
return bitstamp_json return bitstamp_json
else: # if we get any thing else than http/200
print(f"Error fetching data from Bitstamp API: {response.status_code}") print(f"Error fetching data from Bitstamp API: {response.status_code}")
return empty_json return empty_json
def fetch_bitfinex(): def fetch_bitfinex():
""" """
Bitfinex Bitfinex
Returns: Returns:
str: 5min OHLC data in JSON format. str: 5min OHLC data in JSON format.
""" """
response = requests.get("https://api-pub.bitfinex.com/v2/candles/trade:5m:tBTCUSD/last") response = requests.get(
"https://api-pub.bitfinex.com/v2/candles/trade:5m:tBTCUSD/last"
)
if response.status_code == 200: # check if the request was successful if response.status_code == 200: # check if the request was successful
ohlc_data = response.json() ohlc_data = response.json()
candle_stick_data = { candle_stick_data = {
'exchange': 'bitfinex', "exchange": "bitfinex",
'timestamp': ohlc_data[0], "timestamp": ohlc_data[0],
'open': ohlc_data[1], "open": ohlc_data[1],
'high': ohlc_data[2], "high": ohlc_data[2],
'low': ohlc_data[3], "low": ohlc_data[3],
'close': ohlc_data[4], "close": ohlc_data[4],
'volume_quote': ohlc_data[5], "volume_quote": ohlc_data[5],
'volume_base': 0, # not provided by Bitfinex API "volume_base": 0, # not provided by Bitfinex API
'trades': 0, # not provided by Bitfinex API "trades": 0, # not provided by Bitfinex API
} }
bitfinex_json = json.dumps(candle_stick_data, indent=2) bitfinex_json = json.dumps(candle_stick_data, indent=2)
#print("Bitfinex: OK")
#print(bitfinex_json)
return bitfinex_json return bitfinex_json
else: # if we get any thing else than http/200
print(f"Error fetching data from Bitfinex API: {response.status_code}") print(f"Error fetching data from Bitfinex API: {response.status_code}")
return empty_json return empty_json
def fetch_gemini(): def fetch_gemini():
""" """
Fetch BTCUSD OHLC data from Gemini Fetch BTCUSD OHLC data from Gemini
@ -191,56 +210,58 @@ def fetch_gemini():
if response.status_code == 200: # check if the request was successful if response.status_code == 200: # check if the request was successful
gemini_ohlc = response.json() gemini_ohlc = response.json()
candle_stick_data = { candle_stick_data = {
'exchange': 'gemini', "exchange": "gemini",
'timestamp': gemini_ohlc[0][0], "timestamp": gemini_ohlc[0][0],
'open': gemini_ohlc[0][1], "open": gemini_ohlc[0][1],
'high': gemini_ohlc[0][2], "high": gemini_ohlc[0][2],
'low': gemini_ohlc[0][3], "low": gemini_ohlc[0][3],
'close': gemini_ohlc[0][4], "close": gemini_ohlc[0][4],
'volume_quote': 0, # not provided by Gemini API "volume_quote": 0, # not provided by Gemini API
'volume_base': gemini_ohlc[0][5], "volume_base": gemini_ohlc[0][5],
'trades': 0, # not provided by Gemini API "trades": 0, # not provided by Gemini API
} }
gemini_json = json.dumps(candle_stick_data, indent=2) gemini_json = json.dumps(candle_stick_data, indent=2)
#print("Gemini: OK")
#print(gemini_json)
return gemini_json return gemini_json
else: # if we get any thing else than http/200
print(f"Error fetching data from Gemini API: {response.status_code}") print(f"Error fetching data from Gemini API: {response.status_code}")
return empty_json return empty_json
def fetch_bybit(): def fetch_bybit():
""" """
Fetch BTCUSD OHLC data from Bybit Fetch BTCUSD OHLC data from Bybit
Returns: Returns:
str: 5min OHLC data in JSON format. str: 5min OHLC data in JSON format.
""" """
base_url = 'https://api.bybit.com/v2/public/kline/list?symbol=BTCUSD&interval=5&from=' base_url = (
"https://api.bybit.com/v2/public/kline/list?symbol=BTCUSD&interval=5&from="
)
current_unixtime = int(time.time()) current_unixtime = int(time.time())
last_minute = math.floor(current_unixtime / 60) last_minute = math.floor(current_unixtime / 60)
last_minute_unixtime = str(last_minute * 60 - 300) last_minute_unixtime = str(last_minute * 60 - 300)
query_url = ''.join([base_url, last_minute_unixtime]) query_url = "".join([base_url, last_minute_unixtime])
response = requests.get(query_url) response = requests.get(query_url)
if response.status_code == 200: # check if the request was successful if response.status_code == 200: # check if the request was successful
bybit_ohlc = response.json() bybit_ohlc = response.json()
candle_stick_data = { candle_stick_data = {
'exchange': 'bybit', "exchange": "bybit",
'timestamp': bybit_ohlc['result'][0]['open_time'], "timestamp": bybit_ohlc["result"][0]["open_time"],
'open': bybit_ohlc['result'][0]['open'], "open": bybit_ohlc["result"][0]["open"],
'high': bybit_ohlc['result'][0]['high'], "high": bybit_ohlc["result"][0]["high"],
'low': bybit_ohlc['result'][0]['low'], "low": bybit_ohlc["result"][0]["low"],
'close': bybit_ohlc['result'][0]['close'], "close": bybit_ohlc["result"][0]["close"],
'volume_quote': bybit_ohlc['result'][0]['volume'], "volume_quote": bybit_ohlc["result"][0]["volume"],
'volume_base': bybit_ohlc['result'][0]['turnover'], "volume_base": bybit_ohlc["result"][0]["turnover"],
'trades': 0 "trades": 0,
} }
bybit_json = json.dumps(candle_stick_data, indent=2) bybit_json = json.dumps(candle_stick_data, indent=2)
return bybit_json return bybit_json
else: # if we get any thing else than http/200
print(f"Error fetching data from Bybit API: {response.status_code}") print(f"Error fetching data from Bybit API: {response.status_code}")
return empty_json return empty_json
def write_dict_to_database(in_dict, connection): def write_dict_to_database(in_dict, connection):
""" """
Writes given dict to given database. Writes given dict to given database.
@ -251,20 +272,23 @@ def write_dict_to_database(in_dict, connection):
# use placeholders for the values in the insert statement # use placeholders for the values in the insert statement
insert_query = "insert into ohlc (exchange, timestamp, open, high, low, close, volume_quote, volume_base, trades) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" insert_query = "insert into ohlc (exchange, timestamp, open, high, low, close, volume_quote, volume_base, trades) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
values = (in_dict['exchange'], values = (
in_dict['timestamp'], in_dict["exchange"],
in_dict['open'], in_dict["timestamp"],
in_dict['high'], in_dict["open"],
in_dict['low'], in_dict["high"],
in_dict['close'], in_dict["low"],
in_dict['volume_quote'], in_dict["close"],
in_dict['volume_base'], in_dict["volume_quote"],
in_dict['trades']) in_dict["volume_base"],
in_dict["trades"],
)
## apply lock while writing to database ## apply lock while writing to database
with database_lock: with database_lock:
cursor.execute(insert_query, values) cursor.execute(insert_query, values)
connection.commit() connection.commit()
def get_the_data(): def get_the_data():
""" """
Creates infinite While True loop to fetch OHLC data and save it to database. Creates infinite While True loop to fetch OHLC data and save it to database.
@ -280,6 +304,7 @@ def get_the_data():
print("fetches done at", time.time(), "sleeping now for 290") print("fetches done at", time.time(), "sleeping now for 290")
time.sleep(290) time.sleep(290)
def check_auth(text, signature): def check_auth(text, signature):
""" """
Check signatures against known public keys Check signatures against known public keys
@ -294,80 +319,90 @@ def check_auth(text, signature):
## Create bytes-object from the public in 'value' variable ## Create bytes-object from the public in 'value' variable
## and use it to create VerifyingKey (vk) ## and use it to create VerifyingKey (vk)
public_key_bytes = bytes.fromhex(value) public_key_bytes = bytes.fromhex(value)
verifying_key = ecdsa.VerifyingKey.from_string(public_key_bytes, curve=ecdsa.SECP256k1) verifying_key = ecdsa.VerifyingKey.from_string(
public_key_bytes, curve=ecdsa.SECP256k1
)
try: try:
verifying_key.verify(sig_bytes, bytes(text, 'utf-8')) verifying_key.verify(sig_bytes, bytes(text, "utf-8"))
print('user is', key) print("user is", key)
return True return True
except ecdsa.BadSignatureError: except ecdsa.BadSignatureError:
return False return False
@app.route('/')
@app.route("/")
def get_data(): def get_data():
""" """
Serve the data from the database. Limit the responses by given timestamp. Serve the data from the database. Limit the responses by given timestamp.
The pretty thing is under consideration... The pretty thing is under consideration...
""" """
# Get the time (t) argument from the url" # Get the time (t) argument from the url"
query_timestamp = request.args.get('t') query_timestamp = request.args.get("t")
# Should we make output pretty for curl users? # Should we make output pretty for curl users?
query_pretty = request.args.get('pretty') query_pretty = request.args.get("pretty")
# Authentication header, signatured the query with private key of a user # Authentication header, signatured the query with private key of a user
signature = request.headers.get('auth') signature = request.headers.get("auth")
get_url = request.url get_url = request.url
if not check_auth(get_url, signature): if not check_auth(get_url, signature):
return 'Access denied! Check your keys, maybe.', 403 return "Access denied! Check your keys, maybe.", 403
with database_lock: with database_lock:
btc_db = sqlite3.connect(DATABASE) btc_db = sqlite3.connect(DATABASE)
if query_timestamp: if query_timestamp:
rows = btc_db.execute("SELECT exchange, timestamp, open, high, low, close FROM ohlc WHERE timestamp > ? ORDER BY timestamp", (query_timestamp,)).fetchall() rows = btc_db.execute(
"SELECT exchange, timestamp, open, high, low, close FROM ohlc WHERE timestamp > ? ORDER BY timestamp",
(query_timestamp,),
).fetchall()
else: else:
rows = btc_db.execute('SELECT exchange, timestamp, open, high, low, close FROM ohlc ORDER BY timestamp').fetchall() rows = btc_db.execute(
"SELECT exchange, timestamp, open, high, low, close FROM ohlc ORDER BY timestamp"
).fetchall()
query_timestamp = 0 query_timestamp = 0
data = { data = {"timestamp": time.time(), "rows": rows}
"timestamp": time.time(),
"rows": rows
}
# make sha256 checksum and append it to the data object # make sha256 checksum and append it to the data object
data_shasum = sha256(json.dumps(data).encode('utf-8')).hexdigest() data_shasum = sha256(json.dumps(data).encode("utf-8")).hexdigest()
updated_data = {"shasum": data_shasum} updated_data = {"shasum": data_shasum}
updated_data.update(data) updated_data.update(data)
data = updated_data data = updated_data
# sign the response # sign the response
signature = server_private_key.sign(json.dumps(data).encode('utf-8')) signature = server_private_key.sign(json.dumps(data).encode("utf-8"))
signature_hex = binascii.hexlify(signature).decode('utf-8') signature_hex = binascii.hexlify(signature).decode("utf-8")
data['signature'] = signature_hex data["signature"] = signature_hex
if query_pretty: if query_pretty:
response = json.dumps(data, indent=2, separators=(';\n', ' :')) response = json.dumps(data, indent=2, separators=(";\n", " :"))
else: else:
response = json.dumps(data) response = json.dumps(data)
return response, 200, {'Content-Type': 'application/json'} return response, 200, {"Content-Type": "application/json"}
@app.route('/serverkey')
@app.route("/serverkey")
def give_serverkey(): def give_serverkey():
""" """
Serve the public keys of this instace to the world. Serve the public keys of this instace to the world.
""" """
## This endpoint also under Authentication? ## This endpoint also under Authentication?
signature = request.headers.get('auth') signature = request.headers.get("auth")
get_url = request.url get_url = request.url
if not check_auth(get_url, signature): if not check_auth(get_url, signature):
return 'Access denied! Check your keys, maybe.', 403 return "Access denied! Check your keys, maybe.", 403
return jsonify({'public_key': server_public_key_hex}) return jsonify({"public_key": server_public_key_hex})
if __name__ == '__main__':
if __name__ == "__main__":
# Make sanity checks for the database # Make sanity checks for the database
check_database() check_database()
# Start the data fetching backend process # Get the users public keys
user_publickeys = read_keys()
# Start the data fetching backend process§
fetch_thread = threading.Thread(target=get_the_data) fetch_thread = threading.Thread(target=get_the_data)
fetch_thread.daemon = True fetch_thread.daemon = True
fetch_thread.start() fetch_thread.start()

View File

@ -0,0 +1,5 @@
{
"user_publickeys": {
"user1": "f1debc13fb21fe0eee54525aa4f8aae5733b201c755edaa55f8893c90aa375b261a62eaa3110651ac5d7705d402581256a37508b0a1ca28bd919ea44710d9c88"
}
}