diff --git a/spotbit-plugin/.gitignore b/spotbit-plugin/.gitignore new file mode 100644 index 0000000..31b2fdb --- /dev/null +++ b/spotbit-plugin/.gitignore @@ -0,0 +1,10 @@ +__pycache__ +.pytest_cache +*.pyc +.env +*.egg-info +.DS_Store +node_modules +btcd-conn.json +elmd-conn.json +prevent_mining \ No newline at end of file diff --git a/spotbit-plugin/MANIFEST.in b/spotbit-plugin/MANIFEST.in new file mode 100644 index 0000000..d8134d3 --- /dev/null +++ b/spotbit-plugin/MANIFEST.in @@ -0,0 +1,5 @@ +recursive-include src/ahv15/specterext/spotbit/templates * +recursive-include src/ahv15/specterext/spotbit/static * +recursive-include src/ahv15/specterext/spotbit/*/LC_MESSAGES *.mo +recursive-include src/ahv15/specterext/spotbit/translations/*/LC_MESSAGES *.po +include requirements.txt \ No newline at end of file diff --git a/spotbit-plugin/available_exchanges_currencies.py b/spotbit-plugin/available_exchanges_currencies.py new file mode 100644 index 0000000..89abdfe --- /dev/null +++ b/spotbit-plugin/available_exchanges_currencies.py @@ -0,0 +1,53 @@ +import ccxt as ccxt +from bs4 import BeautifulSoup +import requests +r = requests.get("https://github.com/ccxt/ccxt/tree/master/python/ccxt") +soup = BeautifulSoup(r.content, 'html5lib') +links = soup.findAll('a', attrs={'class': 'js-navigation-open Link--primary'}) +objects = {} +for i in range(6, len(links)): + try: + objects[links[i].text[0:-3] + ] = eval("ccxt." + links[i].text[0:-3] + "()") + except: + print(links[i].text[0:-3] + " is not available.") + continue +exchanges = objects.keys() +currencies = ['USD', 'EUR', 'CAD', 'GBP', 'AUD', 'SEK', 'BRL', 'CZK', 'INR'] +possibilities = {} + + +def get_supported_pair_for(currency, exchange): + result = '' + exchange.load_markets() + market_ids_found = [] + for market in exchange.markets_by_id.keys(): + if ((market[:len('BTC')].upper() == 'BTC' or market[:len('XBT')].upper() == 'XBT' or market[:len('XXBTZCAD')].upper() == 'XXBTZCAD') and market[-len(currency):].upper() == currency.upper() and (len(market) == (3 + len(currency)) or (len(market) == (4 + len(currency)) and (market[3] == '/' or market[3] == '-' or market[3] == '_')))): + market_ids_found.append(market) + if market_ids_found: + market_id = market_ids_found[0] + market = exchange.markets_by_id[exchange.market_id(market_id)] + if market: + result = market['symbol'] + return result + + +for exchange in exchanges: + if objects[exchange].has['fetchOHLCV']: + try: + ticker = get_supported_pair_for('USD', objects[exchange]) + except: + print(exchange + " is not available.") + continue + for currency in currencies: + ticker = get_supported_pair_for(currency, objects[exchange]) + if ticker == '' or ticker == 'BTC/USDT': # incorrect id to ticker mapping USD to USDT + continue + if (currency in possibilities.keys()): + possibilities[currency].append([exchange, ticker]) + else: + possibilities[currency] = [[exchange, ticker]] + +for curr in possibilities.keys(): + for exchange in possibilities[curr]: + print(f"{curr}: exchange: {exchange[0]}") diff --git a/spotbit-plugin/pyproject.toml b/spotbit-plugin/pyproject.toml new file mode 100644 index 0000000..7daa9be --- /dev/null +++ b/spotbit-plugin/pyproject.toml @@ -0,0 +1,5 @@ +[build-system] +requires = [ + "cryptoadvance.specter==1.8.1" +] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/spotbit-plugin/pytest.ini b/spotbit-plugin/pytest.ini new file mode 100644 index 0000000..f8d2ae6 --- /dev/null +++ b/spotbit-plugin/pytest.ini @@ -0,0 +1,11 @@ +[pytest] +norecursedirs = tests/bitcoin* tests/elements* tests/xtestdata_testextensions +log_format = [%(levelname)8s] %(message)s %(name)s (%(filename)s:%(lineno)s) +addopts = --bitcoind-version v22.0.0 --elementsd-version v0.21.0.2 +markers = + slow: mark test as slow. + elm: mark test as elementsd dependent +#log_cli = 1 + +filterwarnings = + ignore::DeprecationWarning:bitbox02[.*] \ No newline at end of file diff --git a/spotbit-plugin/requirements.txt b/spotbit-plugin/requirements.txt new file mode 100644 index 0000000..ab5f19e --- /dev/null +++ b/spotbit-plugin/requirements.txt @@ -0,0 +1,2 @@ +cryptoadvance.specter>=1.12.0 +ccxt>=1.74.49 \ No newline at end of file diff --git a/spotbit-plugin/setup.cfg b/spotbit-plugin/setup.cfg new file mode 100644 index 0000000..3a80188 --- /dev/null +++ b/spotbit-plugin/setup.cfg @@ -0,0 +1,25 @@ +[metadata] +name = ahv15_spotbit +version = 0.0.1 +author = ahv15 +author_email = av.harshith15@gmail.com +description = A small example package +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/ahv15/specterext-spotbit +project_urls = + Bug Tracker = https://github.com/ahv15/specterext-spotbit/issues +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: MIT License + Operating System :: OS Independent + +[options] +include_package_data=true +package_dir = + = src +packages = find_namespace: +python_requires = >=3.6 + +[options.packages.find] +where = src \ No newline at end of file diff --git a/spotbit-plugin/setup.py b/spotbit-plugin/setup.py new file mode 100644 index 0000000..57c026b --- /dev/null +++ b/spotbit-plugin/setup.py @@ -0,0 +1,4 @@ +from setuptools import setup + +if __name__ == "__main__": + setup() \ No newline at end of file diff --git a/spotbit-plugin/src/ahv15/specterext/spotbit/__init__.py b/spotbit-plugin/src/ahv15/specterext/spotbit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/spotbit-plugin/src/ahv15/specterext/spotbit/__main__.py b/spotbit-plugin/src/ahv15/specterext/spotbit/__main__.py new file mode 100644 index 0000000..a031865 --- /dev/null +++ b/spotbit-plugin/src/ahv15/specterext/spotbit/__main__.py @@ -0,0 +1,41 @@ +from cryptoadvance.specter.cli import entry_point +from cryptoadvance.specter.cli.cli_server import server +import logging +import click + +logger = logging.getLogger(__name__) + +@click.group() +def cli(): + pass + +@cli.command() +@click.pass_context +@click.option( + "--host", + default="127.0.0.1", + help="if you specify --host 0.0.0.0 then Spotbit will be available in your local LAN.", +) +@click.option( + "--ssl/--no-ssl", + is_flag=True, + default=False, + help="By default SSL encryption will not be used. Use -ssl to create a self-signed certificate for SSL encryption.", +) +@click.option("--debug/--no-debug", default=None) +@click.option("--filelog/--no-filelog", default=True) +@click.option( + "--config", + default=None, + help="A class which sets reasonable default values.", +) +def start(ctx, host, ssl, debug, filelog, config): + if config == None: + config = "ahv15.specterext.spotbit.config.AppProductionConfig" + ctx.invoke(server, host=host, ssl=ssl, debug=debug, filelog=filelog, port=8080, config=config) + +entry_point.add_command(start) + +if __name__ == "__main__": + entry_point() + \ No newline at end of file diff --git a/spotbit-plugin/src/ahv15/specterext/spotbit/config.py b/spotbit-plugin/src/ahv15/specterext/spotbit/config.py new file mode 100644 index 0000000..4b880ac --- /dev/null +++ b/spotbit-plugin/src/ahv15/specterext/spotbit/config.py @@ -0,0 +1,27 @@ +""" +Here Configuration of your Extension (and maybe your Application) takes place +""" +import os +from cryptoadvance.specter.config import ProductionConfig as SpecterProductionConfig + + +class BaseConfig: + ''' This is a extension-based Config which is used as Base ''' + SPOTBIT_SOMEKEY = "some value" + +class ProductionConfig(BaseConfig): + ''' This is a extension-based Config for Production ''' + pass + + +class AppProductionConfig(SpecterProductionConfig): + ''' The AppProductionConfig class can be used to user this extension as application + ''' + # Where should the User endup if he hits the root of that domain? + ROOT_URL_REDIRECT = "/spc/ext/spotbit" + # I guess this is the only extension which should be available? + EXTENSION_LIST = [ + "ahv15.specterext.spotbit.service" + ] + # You probably also want a different folder here + SPECTER_DATA_FOLDER=os.path.expanduser("~/.spotbit") \ No newline at end of file diff --git a/spotbit-plugin/src/ahv15/specterext/spotbit/controller.py b/spotbit-plugin/src/ahv15/specterext/spotbit/controller.py new file mode 100644 index 0000000..751e2dc --- /dev/null +++ b/spotbit-plugin/src/ahv15/specterext/spotbit/controller.py @@ -0,0 +1,84 @@ +from flask import redirect, render_template, request, url_for +from flask import current_app as app + +from cryptoadvance.specter.specter import Specter +from .service import SpotbitService + +from datetime import datetime + + +spotbit_endpoint = SpotbitService.blueprint + + +def ext() -> SpotbitService: + ''' convenience for getting the extension-object''' + return app.specter.ext["spotbit"] + + +def specter() -> Specter: + ''' convenience for getting the specter-object''' + return app.specter + + +@spotbit_endpoint.route("/") +def index(): + service = ext() + status_info = service.status_info() + return render_template( + "spotbit/index.jinja", status=status_info + ) + + +@spotbit_endpoint.route("/", methods=["POST"]) +def index_post(): + service = ext() + info = request.form.get('pair', "None") + print("Test") + print(info) + if (info == ""): + service.remove_db() + else: + service.remove_exchange(info.strip('][').split(', ')) + status_info = service.status_info() + return render_template( + "spotbit/index.jinja", status=status_info + ) + + +@spotbit_endpoint.route("/hist////") +def historical_exchange_rate(currency, exchange, date_start, date_end): + service = ext() + return (service.historical_exchange_rate(currency, exchange, date_start, date_end)) + + +@spotbit_endpoint.route("/now//") +def current_exchange_rate(currency, exchange): + service = ext() + return (service.current_exchange_rate(currency, exchange)) + + +@spotbit_endpoint.route("/settings", methods=["GET"]) +def settings_get(): + return render_template( + "spotbit/settings.jinja", + ) + + +@spotbit_endpoint.route("/settings", methods=["POST"]) +def settings_post(): + exchange = request.form.get('exchange', "None") + currency1 = request.form.get('currency1', "None") + currency2 = request.form.get('currency2', "None") + currency3 = request.form.get('currency3', "None") + currency4 = request.form.get('currency4', "None") + currency5 = request.form.get('currency5', "None") + currency6 = request.form.get('currency6', "None") + currency7 = request.form.get('currency7', "None") + currency8 = request.form.get('currency8', "None") + currency9 = request.form.get('currency9', "None") + start_date = request.form.get('start_date', "None") + frequencies = request.form.get('frequencies', "1m") + service = ext() + service.init_table([exchange.lower()], [currency1, currency2, currency3, currency4, currency5, currency6, + currency7, currency8, currency9], frequencies, start_date) + return redirect(url_for(f"{ SpotbitService.get_blueprint_name()}.settings_get")) diff --git a/spotbit-plugin/src/ahv15/specterext/spotbit/service.py b/spotbit-plugin/src/ahv15/specterext/spotbit/service.py new file mode 100644 index 0000000..d6365d4 --- /dev/null +++ b/spotbit-plugin/src/ahv15/specterext/spotbit/service.py @@ -0,0 +1,406 @@ +import logging +import ccxt as ccxt +import sqlite3 +from pathlib import Path +import time +from datetime import datetime, timedelta +import threading +from threading import Event +import os +import json +from flask_apscheduler import APScheduler +from cryptoadvance.specter.services.service import Service, devstatus_alpha + +logger = logging.getLogger(__name__) +path = Path("./sb.db") +path_hist = Path("./sb_hist.db") + + +objects = {"bitstamp": ccxt.bitstamp(), "bitfinex": ccxt.bitfinex( +), "coinbase": ccxt.coinbase(), "kraken": ccxt.kraken(), "okcoin": ccxt.okcoin()} +exchanges = ['bitstamp', 'coinbase', 'kraken', 'bitfinex', 'okcoin'] +currencies = ['usd', 'eur'] +history_threads = [] +event = "None" +chosen_exchanges = [] +chosen_currencies = {} +frequency_config = 0 +start_date_config = 0 + + +def is_ms(timestamp): + if timestamp % 1000 == 0: + return True + return False + + +def get_supported_pair_for(currency, exchange): + result = '' + exchange.load_markets() + market_ids_found = [market for market in exchange.markets_by_id.keys() if ((market[:len('BTC')].upper() == 'BTC' or market[:len( + 'XBT')].upper() == 'XBT') and market[-len(currency):].upper() == currency.upper() and len(market) == (3 + len(currency)))] + if market_ids_found: + market_id = market_ids_found[0] + market = exchange.markets_by_id[exchange.market_id(market_id)] + if market: + result = market['symbol'] + return result + + +def clear_threads(event): + if (event != "None"): + event.set() + + +def request_history(objects, exchange, currency, start_date, end_date, frequency, event): + con = sqlite3.connect(path_hist, timeout=10) + cur = con.cursor() + ticker = get_supported_pair_for(currency, objects[exchange]) + while start_date < end_date: + if event.is_set(): + return + candles = objects[exchange].fetch_ohlcv( + ticker, frequency, start_date) + records = [] + dt = None + for line in candles: + dt = None + try: + if is_ms(int(line['timestamp'])): + dt = datetime.fromtimestamp(line['timestamp'] / 1e3) + else: + dt = datetime.fromtimestamp(line['timestamp']) + records.append( + [line['timestamp'], dt, ticker, 0.0, 0.0, 0.0, line['last'], 0.0]) + except TypeError: + if line[0] % 1000 == 0: + dt = datetime.fromtimestamp(line[0] / 1e3) + else: + dt = datetime.fromtimestamp(line[0]) + records.append([line[0], dt, ticker, line[1], + line[2], line[3], line[4], line[5]]) + if (candles != []): + statement = f"INSERT INTO {exchange} (timestamp, datetime, pair, open, high, low, close, volume) VALUES (?, ?, ?, ?, ?, ?, ?, ?);" + cur.executemany(statement, records) + con.commit() + start_date = candles[-1][0] + 60000 + else: + end_date = objects[exchange].milliseconds() + + +def request_history_periodically(histExchanges, currencies, frequency, start_date, event): + for h in histExchanges: + for currency in currencies[h].keys(): + if (currencies[h][currency][0]): + hThread = threading.Thread(target=request_history, args=( + objects, h, currency, objects[h].parse8601(start_date + " 00:00:00"), objects[h].milliseconds(), frequency, event)) + hThread.start() + history_threads.append(hThread) + return history_threads + + +def request_periodically(exchanges, currencies, event): + thread = threading.Thread( + target=request, args=(exchanges, currencies, event)) + thread.start() + return thread + + +def request(exchanges, currencies, event): + while True: + if event.is_set(): + break + candles = [] + for e in exchanges: + for curr in currencies[e].keys(): + if (currencies == "None" or not currencies[e][curr][1]): + continue + ticker = get_supported_pair_for(curr, objects[e]) + if (ticker == ''): + continue + if objects[e].has['fetchOHLCV']: + tframe = '1m' + lim = 1 + try: + candles.append([e, ticker, objects[e].fetch_ohlcv( + symbol=ticker, timeframe=tframe, since=None, limit=lim)]) + except Exception as err: + if "does not have" not in str(err): + print(f"error fetching candle: {e} {curr} {err}") + else: + print("check4") + try: + price = objects[e].fetch_ticker(ticker) + print("check") + print(price) + candles.append( + e, ticker, [[price['timestamp'], 0.0, 0.0, 0.0, price['last'], 0.0]]) + except Exception as err: + print(f"error fetching ticker: {err}") + con = sqlite3.connect(path, timeout=30) + cur = con.cursor() + cur.execute("SELECT name FROM sqlite_master WHERE type='table';") + if (cur.fetchall() == []): + continue + for response in candles: + datetime_ = [] + ts = None + try: + if is_ms(int(response[2][0][0])): + datetime_ = datetime.fromtimestamp( + int(response[2][0][0])/1e3) + else: + datetime_ = datetime.fromtimestamp(int(response[2][0][0])) + except OverflowError as oe: + print(f"{oe} caused by {ts}") + for l in response[2][0]: + if l == None: + l = 0 + statement = "INSERT INTO {} (timestamp, datetime, pair, open, high, low, close, volume) VALUES ({}, '{}', '{}', {}, {}, {}, {}, {});".format( + response[0], response[2][0][0], datetime_, response[1], response[2][0][1], response[2][0][2], response[2][0][3], response[2][0][4], response[2][0][5]) + cur.execute(statement) + con.commit() + time.sleep(60) + + +class SpotbitService(Service): + id = "spotbit" + name = "Spotbit" + icon = "spotbit/img/spotbit_avatar.jpg" + logo = "spotbit/img/logo.jpeg" + desc = "Price Info Service" + has_blueprint = True + blueprint_module = "ahv15.specterext.spotbit.controller" + devstatus = devstatus_alpha + isolated_client = False + sort_priority = 2 + SPECTER_WALLET_ALIAS = "wallet" + + @classmethod + def init_table(cls, exchanges, currencies, frequency, start_date): + global event + path = Path("./sb.db") + path_hist = Path("./sb_hist.db") + if (os.path.exists(path)): + os.remove(path) + if (os.path.exists(path_hist)): + os.remove(path_hist) + config = [exchanges, currencies, frequency, start_date] + config_json = json.dumps(config) + with open("config.json", "w") as config: + config.write(config_json) + p = Path("./sb.db") + con = sqlite3.connect(p) + cur = con.cursor() + for exchange in exchanges: + if (exchange == "None"): + continue + if (exchange not in chosen_exchanges): + chosen_exchanges.append(exchange) + sql = f"CREATE TABLE IF NOT EXISTS {exchange} (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, datetime TEXT, pair TEXT, open REAL, high REAL, low REAL, close REAL, volume REAL)" + print(f"created table for {exchange}") + cur.execute(sql) + con.commit() + con.close() + + p = Path("./sb_hist.db") + con = sqlite3.connect(p) + cur = con.cursor() + for exchange in exchanges: + sql = f"CREATE TABLE IF NOT EXISTS {exchange} (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, datetime TEXT, pair TEXT, open REAL, high REAL, low REAL, close REAL, volume REAL)" + print(f"created table for {exchange}") + cur.execute(sql) + con.commit() + con.close() + + for exchange in exchanges: + if (exchange == "None"): + continue + for currency in currencies: + if (currency == "None"): + continue + if (chosen_currencies.__contains__(exchange)): + if (chosen_currencies[exchange].keys()): + chosen_currencies[exchange][currency] = [True, True] + else: + chosen_currencies[exchange] = {currency: [True, True]} + + clear_threads(event) + event = Event() + request_periodically(chosen_exchanges, chosen_currencies, event) + if (start_date != "None"): + request_history_periodically( + chosen_exchanges, chosen_currencies, frequency, start_date, event) + + def callback_after_serverpy_init_app(self, scheduler: APScheduler): + if (os.path.exists('config.json')): + with open('config.json', 'r') as config: + config_json = json.load(config) + SpotbitService.init_table( + config_json[0], config_json[1], config_json[2], config_json[3]) + + @classmethod + def current_exchange_rate(cls, currency, exchange): + p = Path("./sb.db") + con = sqlite3.connect(p, timeout=5) + cur = con.cursor() + ticker = get_supported_pair_for(currency, objects[exchange]) + if exchange in exchanges: + statement = f"SELECT * FROM {exchange} WHERE pair = '{ticker}' ORDER BY timestamp DESC LIMIT 1;" + try: + cursor = cur.execute(statement) + res = cursor.fetchone() + except sqlite3.OperationalError: + print("database is locked. Cannot access it") + return {'err': 'database locked'} + if res != None: + con.close() + return {'id': res[0], 'timestamp': res[1], 'datetime': res[2], 'currency_pair': res[3], 'open': res[4], 'high': res[5], 'low': res[6], 'close': res[7], 'vol': res[8]} + + @classmethod + def historical_exchange_rate(cls, currency, exchange, date_start, date_end): + p = Path("./sb_hist.db") + con = sqlite3.connect(p, timeout=5) + cur = con.cursor() + ticker = get_supported_pair_for(currency, objects[exchange]) + if (str(date_start)).isdigit(): + date_s = int(date_start) + date_e = int(date_end) + else: + try: + date_s = (datetime.fromisoformat( + date_start.replace("T", " "))).timestamp()*1000 + date_e = (datetime.fromisoformat( + date_end.replace("T", " "))).timestamp()*1000 + except Exception: + return "malformed dates. Provide both dates in the same format: use YYYY-MM-DDTHH:mm:SS or millisecond timestamps" + check = f"SELECT timestamp FROM {exchange} ORDER BY timestamp DESC LIMIT 1;" + cursor = cur.execute(check) + statement = "" + ts = cursor.fetchone() + if ts != None and is_ms(int(ts[0])): + statement = f"SELECT * FROM {exchange} WHERE timestamp > {date_s} AND timestamp < {date_e} AND pair = '{ticker}' ORDER BY timestamp DESC;" + else: + date_s /= 1e3 + date_e /= 1e3 + statement = f"SELECT * FROM {exchange} WHERE timestamp > {date_s} AND timestamp < {date_e} AND pair = '{ticker}';" + while True: + try: + cursor = cur.execute(statement) + break + except sqlite3.OperationalError as oe: + time.sleep(5) + res = cursor.fetchall() + con.close() + return {'columns': ['id', 'timestamp', 'datetime', 'currency_pair', 'open', 'high', 'low', 'close', 'vol'], 'data': res} + + @classmethod + def status_info(cls): + p = Path("./sb.db") + status_info = [] + con = sqlite3.connect(p, timeout=5) + cur = con.cursor() + info_check = False + for exchange in chosen_exchanges: + for currency in chosen_currencies[exchange].keys(): + if (chosen_currencies[exchange][currency][1]): + ticker = get_supported_pair_for( + currency, objects[exchange]) + if (ticker == ''): + status_info.append( + [exchange, currency, 'Not Available']) + else: + try: + statement = f"SELECT * FROM {exchange} WHERE pair = '{ticker}' ORDER BY timestamp DESC LIMIT 1;" + cursor = cur.execute(statement) + res = cursor.fetchone() + except sqlite3.OperationalError: + status_info.append([exchange, currency, 'Syncing']) + info_check = True + if res != None: + difference = ( + datetime.now() - datetime.strptime(res[2], '%Y-%m-%d %H:%M:%S')).total_seconds() + if (difference < 300): + status_info.append( + [exchange, currency, 'Updated']) + else: + status_info.append( + [exchange, currency, 'Syncing']) + else: + if (not info_check): + status_info.append( + [exchange, currency, 'Syncing']) + else: + info_check = False + con.close() + + p = Path("./sb_hist.db") + con = sqlite3.connect(p, timeout=5) + cur = con.cursor() + info_check = False + for exchange in chosen_exchanges: + for currency in chosen_currencies[exchange].keys(): + if (chosen_currencies[exchange][currency][0]): + ticker = get_supported_pair_for( + currency, objects[exchange]) + if (ticker == ''): + status_info.append( + [exchange, currency, 'Not Available']) + else: + try: + statement = f"SELECT * FROM {exchange} WHERE pair = '{ticker}' ORDER BY timestamp DESC LIMIT 1;" + cursor = cur.execute(statement) + res = cursor.fetchone() + except sqlite3.OperationalError: + status_info.append( + [exchange, currency, 'Historical Data is Syncing']) + info_check = True + if res != None: + difference = ( + datetime.now() - datetime.strptime(res[2], '%Y-%m-%d %H:%M:%S')).total_seconds() + if (abs(difference) < 3600): + status_info.append( + [exchange, currency, 'Historical Data is Updated']) + else: + status_info.append( + [exchange, currency, 'Historical Data is Syncing']) + else: + if (not info_check): + status_info.append( + [exchange, currency, 'Historical Data is Syncing']) + else: + info_check = False + return status_info + + @classmethod + def remove_exchange(cls, info): + global event + clear_threads(event) + event = Event() + with open('config.json', 'r') as config: + config_json = json.load(config) + + if (info[2].strip('\'')[0] == 'H'): + chosen_currencies[info[0].strip( + '\'')][info[1].strip('\'')][0] = False + else: + chosen_currencies[info[0].strip( + '\'')][info[1].strip('\'')][1] = False + request_periodically(chosen_exchanges, chosen_currencies, event) + request_history_periodically( + chosen_exchanges, chosen_currencies, config_json[2], config_json[3], event) + + @classmethod + def remove_db(cls): + global event + global chosen_currencies + global chosen_exchanges + clear_threads(event) + event = Event() + time.sleep(1) + chosen_exchanges = [] + chosen_currencies = {} + path = Path("./sb.db") + path_hist = Path("./sb_hist.db") + os.remove(path) + os.remove(path_hist) diff --git a/spotbit-plugin/src/ahv15/specterext/spotbit/static/spotbit/css/styles.css b/spotbit-plugin/src/ahv15/specterext/spotbit/static/spotbit/css/styles.css new file mode 100644 index 0000000..de6f013 --- /dev/null +++ b/spotbit-plugin/src/ahv15/specterext/spotbit/static/spotbit/css/styles.css @@ -0,0 +1 @@ +/* This is the place to put all your styles */ diff --git a/spotbit-plugin/src/ahv15/specterext/spotbit/static/spotbit/img/close.svg b/spotbit-plugin/src/ahv15/specterext/spotbit/static/spotbit/img/close.svg new file mode 100644 index 0000000..989837c --- /dev/null +++ b/spotbit-plugin/src/ahv15/specterext/spotbit/static/spotbit/img/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spotbit-plugin/src/ahv15/specterext/spotbit/static/spotbit/img/logo.jpeg b/spotbit-plugin/src/ahv15/specterext/spotbit/static/spotbit/img/logo.jpeg new file mode 100644 index 0000000..06407e9 Binary files /dev/null and b/spotbit-plugin/src/ahv15/specterext/spotbit/static/spotbit/img/logo.jpeg differ diff --git a/spotbit-plugin/src/ahv15/specterext/spotbit/static/spotbit/img/spotbit_avatar.jpg b/spotbit-plugin/src/ahv15/specterext/spotbit/static/spotbit/img/spotbit_avatar.jpg new file mode 100644 index 0000000..e4f4c51 Binary files /dev/null and b/spotbit-plugin/src/ahv15/specterext/spotbit/static/spotbit/img/spotbit_avatar.jpg differ diff --git a/spotbit-plugin/src/ahv15/specterext/spotbit/static/spotbit/img/stop_symbol.svg b/spotbit-plugin/src/ahv15/specterext/spotbit/static/spotbit/img/stop_symbol.svg new file mode 100644 index 0000000..05bc574 --- /dev/null +++ b/spotbit-plugin/src/ahv15/specterext/spotbit/static/spotbit/img/stop_symbol.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spotbit-plugin/src/ahv15/specterext/spotbit/templates/spotbit/base.jinja b/spotbit-plugin/src/ahv15/specterext/spotbit/templates/spotbit/base.jinja new file mode 100644 index 0000000..1cbb52f --- /dev/null +++ b/spotbit-plugin/src/ahv15/specterext/spotbit/templates/spotbit/base.jinja @@ -0,0 +1,4 @@ +{% extends "base.jinja" %} +{% block head %} + +{% endblock %} \ No newline at end of file diff --git a/spotbit-plugin/src/ahv15/specterext/spotbit/templates/spotbit/components/spotbit_menu.jinja b/spotbit-plugin/src/ahv15/specterext/spotbit/templates/spotbit/components/spotbit_menu.jinja new file mode 100644 index 0000000..55852cc --- /dev/null +++ b/spotbit-plugin/src/ahv15/specterext/spotbit/templates/spotbit/components/spotbit_menu.jinja @@ -0,0 +1,16 @@ +{% from 'components/menu_item.jinja' import menu_item %} + +{# + spotbit_menu - Tabs menu to navigate between the spotbit screens. + Parameters: + - active_menuitem: Current active tab. Options: 'general', 'settings', ... + #} +{% macro spotbit_menu(active_menuitem) -%} + +{%- endmacro %} \ No newline at end of file diff --git a/spotbit-plugin/src/ahv15/specterext/spotbit/templates/spotbit/components/spotbit_tab.jinja b/spotbit-plugin/src/ahv15/specterext/spotbit/templates/spotbit/components/spotbit_tab.jinja new file mode 100644 index 0000000..0ea075d --- /dev/null +++ b/spotbit-plugin/src/ahv15/specterext/spotbit/templates/spotbit/components/spotbit_tab.jinja @@ -0,0 +1,8 @@ +{% extends "spotbit/base.jinja" %} +{% block main %} + + {% from 'spotbit/components/spotbit_menu.jinja' import spotbit_menu with context %} + {{ spotbit_menu(tab) }} + {% block content %} + {% endblock %} +{% endblock %} \ No newline at end of file diff --git a/spotbit-plugin/src/ahv15/specterext/spotbit/templates/spotbit/index.jinja b/spotbit-plugin/src/ahv15/specterext/spotbit/templates/spotbit/index.jinja new file mode 100644 index 0000000..763e32f --- /dev/null +++ b/spotbit-plugin/src/ahv15/specterext/spotbit/templates/spotbit/index.jinja @@ -0,0 +1,187 @@ +{% extends "spotbit/components/spotbit_tab.jinja" %} +{% block title %}Settings{% endblock %} +{% set tab = 'index' %} +{% block content %} + +

+
+
+ + + + + + + + + + + {% for element in status %} + + + + + + + + + + + {% endfor %} + +
+ + + ExchangeCurrencyStatus
+ + + + {{ element[0] }} + + {{ element[1] }} + + {{ element[2] }} +
+
+
+{% endblock %} + + + +{% block scripts %} + +{% endblock %} diff --git a/spotbit-plugin/src/ahv15/specterext/spotbit/templates/spotbit/settings.jinja b/spotbit-plugin/src/ahv15/specterext/spotbit/templates/spotbit/settings.jinja new file mode 100644 index 0000000..fd4db0d --- /dev/null +++ b/spotbit-plugin/src/ahv15/specterext/spotbit/templates/spotbit/settings.jinja @@ -0,0 +1,321 @@ +{% extends "spotbit/components/spotbit_tab.jinja" %} +{% block title %}Settings{% endblock %} +{% set tab = 'settings_get' %} +{% block content %} +
+ + +
+

{{ _("Configure your extension") }}

+ +
+
+
+ Select Currencies + +
+ {% set currencies = ['USD', 'EUR', 'CAD', 'GBP', 'AUD', 'SEK', 'BRL', 'CZK', 'INR']%} +
    + {% for item in currencies %} +
  • + + {{ item }} +
  • + {% endfor %} +
+
+ + + +
+ + + + + + + + + +
+ {{ _("Select Exchange (disabled if no currencies are chosen):") }} +
+
+ +
+
+
+ +

Configure your historical data below:


+ + + + + +
+ + + + + + + + + +
+ {{ _("Frequency:") }} +
+
+ +
+
+
+ +
+ +
+ +
+
+
+
+
+ +{% endblock %} + + + +{% block scripts %} + + +{% endblock %} \ No newline at end of file diff --git a/spotbit-plugin/tests/conftest.py b/spotbit-plugin/tests/conftest.py new file mode 100644 index 0000000..461cef7 --- /dev/null +++ b/spotbit-plugin/tests/conftest.py @@ -0,0 +1,539 @@ +import atexit +import code +import json +import logging +import os +import signal +import sys +import tempfile +import traceback + +import pytest +from cryptoadvance.specter.config import TestConfig +from cryptoadvance.specter.managers.device_manager import DeviceManager +from cryptoadvance.specter.managers.user_manager import UserManager +from cryptoadvance.specter.process_controller.bitcoind_controller import ( + BitcoindPlainController, +) +from cryptoadvance.specter.process_controller.elementsd_controller import ( + ElementsPlainController, +) +from cryptoadvance.specter.server import SpecterFlask, create_app, init_app +from cryptoadvance.specter.specter import Specter +from cryptoadvance.specter.specter_error import SpecterError +from cryptoadvance.specter.user import User, hash_password +from cryptoadvance.specter.util.common import str2bool +from cryptoadvance.specter.util.shell import which +from cryptoadvance.specter.util.wallet_importer import WalletImporter + +logger = logging.getLogger(__name__) + +pytest_plugins = [ + "fix_ghost_machine", + "fix_keys_and_seeds", + "fix_devices_and_wallets", + "fix_testnet", +] + +# This is from https://stackoverflow.com/questions/132058/showing-the-stack-trace-from-a-running-python-application +# it enables stopping a hanging test via sending the pytest-process a SIGUSR2 (12) +# kill 12 pid-of-pytest +# In the article they claim to open a debug-console which didn't work for me but at least +# you get a stacktrace in the output. +def debug(sig, frame): + """Interrupt running process, and provide a python prompt for + interactive debugging.""" + d = {"_frame": frame} # Allow access to frame object. + d.update(frame.f_globals) # Unless shadowed by global + d.update(frame.f_locals) + + i = code.InteractiveConsole(d) + message = "Signal received : entering python shell.\nTraceback:\n" + message += "".join(traceback.format_stack(frame)) + i.interact(message) + + +def listen(): + signal.signal(signal.SIGUSR2, debug) # Register handler + + +def pytest_addoption(parser): + """Internally called to add options to pytest + see pytest_generate_tests(metafunc) on how to check that + Also used to register the SIGUSR2 (12) as decribed in conftest.py + """ + parser.addoption("--docker", action="store_true", help="run bitcoind in docker") + parser.addoption( + "--bitcoind-version", + action="store", + default="v0.20.1", + help="Version of bitcoind (something which works with git checkout ...)", + ) + parser.addoption( + "--bitcoind-log-stdout", + action="store", + default=False, + help="Whether bitcoind should log to stdout (default:False)", + ) + parser.addoption( + "--elementsd-version", + action="store", + default="master", + help="Version of elementsd (something which works with git checkout ...)", + ) + listen() + + +def pytest_generate_tests(metafunc): + # ToDo: use custom compiled version of bitcoind + # E.g. test again bitcoind version [currentRelease] + master-branch + if "docker" in metafunc.fixturenames: + if metafunc.config.getoption("docker"): + # That's a list because we could do both (see above) but currently that doesn't make sense in that context + metafunc.parametrize("docker", [True], scope="session") + else: + metafunc.parametrize("docker", [False], scope="session") + + +def instantiate_bitcoind_controller(docker, request, rpcport=18543, extra_args=[]): + # logging.getLogger().setLevel(logging.DEBUG) + requested_version = request.config.getoption("--bitcoind-version") + log_stdout = str2bool(request.config.getoption("--bitcoind-log-stdout")) + if docker: + from cryptoadvance.specter.process_controller.bitcoind_docker_controller import ( + BitcoindDockerController, + ) + + bitcoind_controller = BitcoindDockerController( + rpcport=rpcport, docker_tag=requested_version + ) + else: + if os.path.isfile("tests/bitcoin/src/bitcoind"): + bitcoind_controller = BitcoindPlainController( + bitcoind_path="tests/bitcoin/src/bitcoind", rpcport=rpcport + ) # always prefer the self-compiled bitcoind if existing + elif os.path.isfile("tests/bitcoin/bin/bitcoind"): + bitcoind_controller = BitcoindPlainController( + bitcoind_path="tests/bitcoin/bin/bitcoind", rpcport=rpcport + ) # next take the self-installed binary if existing + else: + bitcoind_controller = BitcoindPlainController( + rpcport=rpcport + ) # Alternatively take the one on the path for now + bitcoind_controller.start_bitcoind( + cleanup_at_exit=True, + cleanup_hard=True, + extra_args=extra_args, + log_stdout=log_stdout, + ) + assert not bitcoind_controller.datadir is None + running_version = bitcoind_controller.version() + requested_version = request.config.getoption("--bitcoind-version") + assert running_version == requested_version, ( + "Please make sure that the Bitcoind-version (%s) matches with the version in pytest.ini (%s)" + % (running_version, requested_version) + ) + return bitcoind_controller + + +def instantiate_elementsd_controller(request, rpcport=18643, extra_args=[]): + if os.path.isfile("tests/elements/src/elementsd"): + elementsd_controller = ElementsPlainController( + elementsd_path="tests/elements/src/elementsd", rpcport=rpcport + ) # always prefer the self-compiled bitcoind if existing + elif os.path.isfile("tests/elements/bin/elementsd"): + elementsd_controller = ElementsPlainController( + elementsd_path="tests/elements/bin/elementsd", rpcport=rpcport + ) # next take the self-installed binary if existing + else: + elementsd_controller = ElementsPlainController( + rpcport=rpcport + ) # Alternatively take the one on the path for now + elementsd_controller.start_elementsd( + cleanup_at_exit=True, cleanup_hard=True, extra_args=extra_args + ) + assert not elementsd_controller.datadir is None + running_version = elementsd_controller.version() + requested_version = request.config.getoption("--elementsd-version") + assert running_version == requested_version, ( + "Please make sure that the elementsd-version (%s) matches with the version in pytest.ini (%s)" + % (running_version, requested_version) + ) + return elementsd_controller + + +# Below this point are fixtures. Fixtures have a scope. Check about scopes here: +# https://docs.pytest.org/en/6.2.x/fixture.html#scope-sharing-fixtures-across-classes-modules-packages-or-session +# possible values: function, class, module, package or session. +# The nodes are of scope session. All else is the default (function) + + +@pytest.fixture(scope="session") +def bitcoind_path(): + if os.path.isfile("tests/bitcoin/src/bitcoind"): + return "tests/bitcoin/src/bitcoind" + elif os.path.isfile("tests/bitcoin/bin/bitcoind"): + return "tests/bitcoin/bin/bitcoind" + else: + return which("bitcoind") + + +@pytest.fixture(scope="session") +def bitcoin_regtest(docker, request): + bitcoind_regtest = instantiate_bitcoind_controller(docker, request, extra_args=None) + try: + assert bitcoind_regtest.get_rpc().test_connection() + assert not bitcoind_regtest.datadir is None + yield bitcoind_regtest + finally: + bitcoind_regtest.stop_bitcoind() + + +@pytest.fixture(scope="session") +def elements_elreg(request): + elements_elreg = instantiate_elementsd_controller(request, extra_args=None) + try: + yield elements_elreg + assert not elements_elreg.datadir is None + finally: + elements_elreg.stop_elementsd() + + +@pytest.fixture +def empty_data_folder(): + # Make sure that this folder never ever gets a reasonable non-testing use-case + with tempfile.TemporaryDirectory(prefix="specter_home_tmp_") as data_folder: + yield data_folder + + +@pytest.fixture +def devices_filled_data_folder(empty_data_folder): + os.makedirs(empty_data_folder + "/devices") + with open(empty_data_folder + "/devices/trezor.json", "w") as text_file: + text_file.write( + """ +{ + "name": "Trezor", + "type": "trezor", + "keys": [ + { + "derivation": "m/49h/0h/0h", + "original": "ypub6XFn7hfb676MLm6ZsAviuQKXeRDNgT9Bs32KpRDPnkKgKDjKcrhYCXJ88aBfy8co2k9eujugJX5nwq7RPG4sj6yncDEPWN9dQGmFWPy4kFB", + "fingerprint": "1ef4e492", + "type": "sh-wpkh", + "xpub": "xpub6CRWp2zfwRYsVTuT2p96hKE2UT4vjq9gwvW732KWQjwoG7v6NCXyaTdz7NE5yDxsd72rAGK7qrjF4YVrfZervsJBjsXxvTL98Yhc7poBk7K" + }, + { + "derivation": "m/84h/0h/0h", + "original": "zpub6rGoJTXEhKw7hUFkjMqNctTzojkzRPa3VFuUWAirqpuj13mRweRmnYpGD1aQVFpxNfp17zVU9r7F6oR3c4zL3DjXHdewVvA7kjugHSqz5au", + "fingerprint": "1ef4e492", + "type": "wpkh", + "xpub": "xpub6CcGh8BQPxr9zssX4eG8CiGzToU6Y9b3f2s2wNw65p9xtr8ySL6eYRVzAbfEVSX7ZPaPd3JMEXQ9LEBvAgAJSkNKYxG6L6X9DHnPWNQud4H" + }, + { + "derivation": "m/48h/0h/0h/1h", + "original": "Ypub6jtWQ1r2D7EwqNoxERU28MWZH4WdL3pWdN8guFJRBTmGwstJGzMXJe1VaNZEuAAVsZwpKPhs5GzNPEZR77mmX1mjwzEiouxmQYsrxFBNVNN", + "fingerprint": "1ef4e492", + "type": "sh-wsh", + "xpub": "xpub6EA9y7SfVU96ZWTTTQDR6C5FPJKvB59RPyxoCb8zRgYzGbWAFvogbTVRkTeBLpHgETm2hL7BjQFKNnL66CCoaHyUFBRtpbgHF6YLyi7fr6m" + }, + { + "derivation": "m/48h/0h/0h/2h", + "original": "Zpub74imhgWwMnnRkSPkiNavCQtSBu1fGo8RP96h9eT2GHCgN5eFU9mZVPhGphvGnG26A1cwJxtkmbHR6nLeTw4okpCDjZCEj2HRLJoVHAEsch9", + "fingerprint": "1ef4e492", + "type": "wsh", + "xpub": "xpub6EA9y7SfVU96dGr96zYgxAMd8AgWBCTqEeQafbPi8VcWdhStCS4AA9X4yb3dE1VM7GKLwRhWy4BpD3VkjK5q1riMAQgz9oBSu8QKv5S7KzD" + }, + { + "derivation": "m/49h/1h/0h", + "original": "upub5EKoQv21nQNkhdt4yuLyRnWitA3EGhW1ru1Y8VTG8gdys2JZhqiYkhn4LHp2heHnH41kz95bXPvrYVRuFUrdUMik6YdjFV4uL4EubnesttQ", + "fingerprint": "1ef4e492", + "type": "sh-wpkh", + "xpub": "tpubDDCDr9rSwixeXKeGwAgwFy8bjBaE5wya9sAVqEC4ccXWmcQxY34KmLRJdwmaDsCnHsu5r9P9SUpYtXmCoRwukWDqmAUJgkBbjC2FXUzicn6" + }, + { + "derivation": "m/84h/1h/0h", + "original": "vpub5Y35MNUT8sUR2SnRCU9A9S6z1JDACMTuNnM8WHXvuS7hCwuVuoRAWJGpi66Yo8evGPiecN26oLqx19xf57mqVQjiYb9hbb4QzbNmFfsS9ko", + "fingerprint": "1ef4e492", + "type": "wpkh", + "xpub": "tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc" + }, + { + "derivation": "m/48h/1h/0h/1h", + "original": "Upub5Tk9tZtdzVaTGWtygRTKDDmaN5vfB59pn2L5MQyH6BkVpg2Y5J95rtpQndjmXNs3LNFiy8zxpHCTtvxxeePjgipF7moTHQZhe3E5uPzDXh8", + "fingerprint": "1ef4e492", + "type": "sh-wsh", + "xpub": "tpubDFiVCZzdarbyfdVoh2LJDL3eVKRPmxwnkiqN8tSYCLod75a2966anQbjHajqVAZ97j54xZJPr9hf7ogVuNL4pPCfwvXdKGDQ9SjZF7vXQu1" + }, + { + "derivation": "m/48h/1h/0h/2h", + "original": "Vpub5naRCEZZ9B7wCKLWuqoNdg6ddWEx8ruztUygXFZDJtW5LRMqUP5HV2TsNw1nc74Ba3QPDSH7qzauZ8LdfNmnmofpfmztCGPgP7vaaYSmpgN", + "fingerprint": "1ef4e492", + "type": "wsh", + "xpub": "tpubDFiVCZzdarbyk8kE65tjRhHCambEo8iTx4xkXL8b33BKZj66HWsDnUb3rg4GZz6Mwm6vTNyzRCjYtiScCQJ77ENedb2deDDtcoNQXiUouJQ" + } + ] +} +""" + ) + with open(empty_data_folder + "/devices/specter.json", "w") as text_file: + text_file.write( + """ +{ + "name": "Specter", + "type": "specter", + "keys": [ + { + "derivation": "m/48h/1h/0h/2h", + "original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", + "fingerprint": "08686ac6", + "type": "wsh", + "xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL" + }, + { + "derivation": "m/84h/1h/0h", + "original": "vpub5ZSem3mLXiSJzgDX6pJb2N9L6sJ8m6ejaksLPLSuB53LBzCi2mMsBg19eEUSDkHtyYp75GATjLgt5p3S43WjaVCXAWU9q9H5GhkwJBrMiAb", + "fingerprint": "08686ac6", + "type": "wpkh", + "xpub": "tpubDDUotcvrYMUiy4ncDirveTfhmvggdj8nxcW5JgHpGzYz3UVscJY5aEzFvgUPk4YyajadBnsTBmE2YZmAtJC14Q21xncJgVaHQ7UdqMRVRbU" + }, + { + "derivation": "m/84h/1h/1h", + "original": "vpub5ZSem3mLXiSK55jPzfLVhbHbTEwGzEFZv3xrGFCw1vGHSNw7WcVuJXysJLWcgENQd3iXSNQaeSXUBW55Hy4GAjSTjrWP4vpKKkUN9jiU1Tc", + "fingerprint": "08686ac6", + "type": "wpkh", + "xpub": "tpubDDUotcvrYMUj3UJV7ZtqKgoy8JKprrjdHubbBb3r7qmwHsEH69g7h6xyanWaCYdVEEV3Yu7a6s4ceFnp8DjXeeFxY8eXvH7XTAC4gxfDNEW" + }, + { + "derivation": "m/84h/1h/2h", + "original": "vpub5ZSem3mLXiSK64v64deytnDCoYqbUSYHvmVurUGVMEnXMyEybtF3FEnNuiFDDC6J18a81fv5ptQXaQaaRiYx8MRxahipgxPLdxubpYt1dkD", + "fingerprint": "08686ac6", + "type": "wpkh", + "xpub": "tpubDDUotcvrYMUj4TVBBYDKWsjaUcE9M52MJd8emp7QTAJBDTY9BRRFdomVCAFAjWMNcKLe8Cd5HJwg3AJKFyEDcGFTNyryYJgYmNdJMhwB2RG" + }, + { + "derivation": "m/84h/1h/3h", + "original": "vpub5ZSem3mLXiSK8cKzh4sHxTvN7mgYQA29HfoAZeCDtX1M2zdejN5XVAtVyqhk8eui18JTtZ9M3VD3AiWCz8VwrybhBUh3HxzS8js3mLVybDT", + "fingerprint": "08686ac6", + "type": "wpkh", + "xpub": "tpubDDUotcvrYMUj6zu5oyRdaZSjnq56GnWCfXRuUz38zSWztUvpJuFjsjscGHhheyAncK4z15rLVukBdUDwpPBDLtRBykqC9KHeG9akJWRipKK" + } + ] +} +""" + ) + return empty_data_folder # no longer empty, though + + +@pytest.fixture +def wallets_filled_data_folder(devices_filled_data_folder): + simple_json = """ +{ + "alias": "simple", + "fullpath": "/home/kim/.specter/wallets/regtest/simple.json", + "name": "Simple", + "address_index": 0, + "keypool": 5, + "address": "bcrt1qcatuhg0gll3h7py4cmn53rjjn9xlsqfwj3zcej", + "change_index": 0, + "change_address": "bcrt1qt28v03278lmmxllys89acddp2p5y4zds94944n", + "change_keypool": 5, + "type": "simple", + "description": "Single (Segwit)", + "keys": [{ + "derivation": "m/84h/1h/0h", + "original": "vpub5Y35MNUT8sUR2SnRCU9A9S6z1JDACMTuNnM8WHXvuS7hCwuVuoRAWJGpi66Yo8evGPiecN26oLqx19xf57mqVQjiYb9hbb4QzbNmFfsS9ko", + "fingerprint": "1ef4e492", + "type": "wpkh", + "xpub": "tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc" + }], + "recv_descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/0/*)#xp8lv5nr", + "change_descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/1/*)#h4z73prm", + "device": "Trezor", + "device_type": "trezor", + "address_type": "bech32" +} +""" + another_wallet_json = """ +{ + "name": "sdsd", + "alias": "sdsd", + "description": "Single (Segwit)", + "address_type": "bech32", + "address": "bcrt1q4h86vfanswhsle63hw2muv9h5a45cg2878uez5", + "address_index": 0, + "change_address": "bcrt1qxsj28ddr95xvp7xjyzkkfq6qknrn4kap30zkut", + "change_index": 0, + "keypool": 60, + "change_keypool": 20, + "recv_descriptor": "wpkh([41490ec7/84h/1h/0h]tpubDCTPz7KwyetfhQNMSWiK34pPR2zSTsTybrMPgRVAzouNLqtgsv51o81KjccmTbjkWJ8mVhRJM1LxZD6AfRH2635tHpHeCAKW446iwADNv7C/0/*)#rn833s5g", + "change_descriptor": "wpkh([41490ec7/84h/1h/0h]tpubDCTPz7KwyetfhQNMSWiK34pPR2zSTsTybrMPgRVAzouNLqtgsv51o81KjccmTbjkWJ8mVhRJM1LxZD6AfRH2635tHpHeCAKW446iwADNv7C/1/*)#j8zsv9ys", + "keys": [ + { + "original": "vpub5YRErYARy1rFj1oGKc9yQyJ1jybtbEyvDziem5eFttPiVMbXJNtoQZ2DTAcowHUfu7NFPAiJtaop6TNRqAbkc8GPVY9VLp2HveP2PygjuYh", + "fingerprint": "41490ec7", + "derivation": "m/84h/1h/0h", + "type": "wpkh", + "purpose": "#0 Single Sig (Segwit)", + "xpub": "tpubDCTPz7KwyetfhQNMSWiK34pPR2zSTsTybrMPgRVAzouNLqtgsv51o81KjccmTbjkWJ8mVhRJM1LxZD6AfRH2635tHpHeCAKW446iwADNv7C" + } + ], + "devices": [ + "dsds" + ], + "sigs_required": 1, + "blockheight": 0, + "pending_psbts": {}, + "frozen_utxo": [], + "last_block": "187e2db380eb6d901efd87188f00c7074506c9c3813b8ecec7300ecc4e55eb46" +} +""" + + os.makedirs(os.path.join(devices_filled_data_folder, "wallets", "regtest")) + with open( + os.path.join(devices_filled_data_folder, "wallets", "regtest", "simple.json"), + "w", + ) as json_file: + json_file.write(simple_json) + os.makedirs(os.path.join(devices_filled_data_folder, "wallets_someuser", "regtest")) + with open( + os.path.join( + devices_filled_data_folder, "wallets_someuser", "regtest", "simple.json" + ), + "w", + ) as json_file: + json_file.write(another_wallet_json) + return devices_filled_data_folder # and with wallets obviously + + +@pytest.fixture +def device_manager(devices_filled_data_folder): + return DeviceManager(os.path.join(devices_filled_data_folder, "devices")) + + +# @pytest.fixture +# def user_manager(empty_data_folder) -> UserManager: +# """A UserManager having users alice, bob and eve""" +# specter = Specter(data_folder=empty_data_folder) +# user_manager = UserManager(specter=specter) +# config = {} +# user_manager.get_user("admin").decrypt_user_secret("admin") +# user_manager.create_user( +# user_id="alice", +# username="alice", +# plaintext_password="plain_pass_alice", +# config=config, +# ) +# user_manager.create_user( +# user_id="bob", +# username="bob", +# plaintext_password="plain_pass_bob", +# config=config, +# ) +# user_manager.create_user( +# user_id="eve", +# username="eve", +# plaintext_password="plain_pass_eve", +# config=config, +# ) +# return user_manager + + +@pytest.fixture +def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder): + assert bitcoin_regtest.get_rpc().test_connection() + config = { + "rpc": { + "autodetect": False, + "datadir": "", + "user": bitcoin_regtest.rpcconn.rpcuser, + "password": bitcoin_regtest.rpcconn.rpcpassword, + "port": bitcoin_regtest.rpcconn.rpcport, + "host": bitcoin_regtest.rpcconn.ipaddress, + "protocol": "http", + }, + "auth": { + "method": "rpcpasswordaspin", + }, + } + specter = Specter(data_folder=devices_filled_data_folder, config=config) + assert specter.chain == "regtest" + # Create a User + someuser = specter.user_manager.add_user( + User.from_json( + user_dict={ + "id": "someuser", + "username": "someuser", + "password": hash_password("somepassword"), + "config": {}, + "is_admin": False, + "services": None, + }, + specter=specter, + ) + ) + specter.user_manager.save() + specter.check() + + assert not specter.wallet_manager.working_folder is None + try: + yield specter + finally: + # Deleting all Wallets (this will also purge them on core) + for user in specter.user_manager.users: + for wallet in list(user.wallet_manager.wallets.values()): + user.wallet_manager.delete_wallet( + wallet, bitcoin_datadir=bitcoin_regtest.datadir, chain="regtest" + ) + + +def specter_app_with_config(config={}, specter=None): + """helper-function to create SpecterFlasks""" + if isinstance(config, dict): + tempClass = type("tempClass", (TestConfig,), {}) + for key, value in config.items(): + setattr(tempClass, key, value) + # service_manager will expect the class to be defined as a direct property of the module: + if hasattr(sys.modules[__name__], "tempClass"): + delattr(sys.modules[__name__], "tempClass") + assert not hasattr(sys.modules[__name__], "tempClass") + setattr(sys.modules[__name__], "tempClass", tempClass) + assert hasattr(sys.modules[__name__], "tempClass") + assert getattr(sys.modules[__name__], "tempClass") == tempClass + config = tempClass + app = create_app(config=config) + app.app_context().push() + app.config["TESTING"] = True + app.testing = True + app.tor_service_id = None + app.tor_enabled = False + init_app(app, specter=specter) + return app + + +@pytest.fixture +def app(specter_regtest_configured) -> SpecterFlask: + """the Flask-App, but uninitialized""" + return specter_app_with_config( + config="cryptoadvance.specter.config.TestConfig", + specter=specter_regtest_configured, + ) + + +@pytest.fixture +def app_no_node(empty_data_folder) -> SpecterFlask: + specter = Specter(data_folder=empty_data_folder) + app = create_app(config="cryptoadvance.specter.config.TestConfig") + app.app_context().push() + app.config["TESTING"] = True + app.testing = True + app.tor_service_id = None + app.tor_enabled = False + init_app(app, specter=specter) + return app + + +@pytest.fixture +def client(app): + """a test_client from an initialized Flask-App""" + return app.test_client() \ No newline at end of file diff --git a/spotbit-plugin/tests/fix_devices_and_wallets.py b/spotbit-plugin/tests/fix_devices_and_wallets.py new file mode 100644 index 0000000..ac5c46f --- /dev/null +++ b/spotbit-plugin/tests/fix_devices_and_wallets.py @@ -0,0 +1,118 @@ +import pytest +import random +import time + +from cryptoadvance.specter.util.mnemonic import generate_mnemonic +from cryptoadvance.specter.process_controller.node_controller import NodeController +from cryptoadvance.specter.specter import Specter +from cryptoadvance.specter.wallet import Wallet, Device + + +def create_hot_wallet_device( + specter_regtest_configured, name=None, mnemonic=None +) -> Device: + if mnemonic == None: + mnemonic = generate_mnemonic(strength=128) + if name == None: + name = "_".join(mnemonic.split(" ")[0:3]) + wallet_manager = specter_regtest_configured.wallet_manager + device_manager = specter_regtest_configured.device_manager + + # Create the device + device = device_manager.add_device(name=name, device_type="bitcoincore", keys=[]) + device.setup_device(file_password=None, wallet_manager=wallet_manager) + device.add_hot_wallet_keys( + mnemonic=mnemonic, + passphrase="", + paths=[ + "m/49h/1h/0h", # Single Sig (Nested) + "m/84h/1h/0h", # Single Sig (Segwit)' + "m/86h/1h/0h", # Single Sig (Taproot) # Taproot ONLY works if this derivation path is enabled. The wallet descriptor is derived from this + "m/48h/1h/0h/1h", # Multisig Sig (Nested) + "m/48h/1h/0h/2h", # Multisig Sig (Segwit) + # "m/44h/0h/0h", # pkh single-legacy + ], + file_password=None, + wallet_manager=wallet_manager, + testnet=True, + keys_range=[0, 1000], + keys_purposes=[], + ) + return device + + +@pytest.fixture +def hot_wallet_device_1(specter_regtest_configured): + return create_hot_wallet_device(specter_regtest_configured) + + +@pytest.fixture +def hot_wallet_device_2(specter_regtest_configured): + return create_hot_wallet_device(specter_regtest_configured) + + +def create_hot_wallet_with_ID( + specter_regtest_configured: Specter, hot_wallet_device_1, wallet_id +) -> Wallet: + device = hot_wallet_device_1 + wallet_manager = specter_regtest_configured.wallet_manager + assert device.taproot_available(specter_regtest_configured.rpc) + + # create the wallet + keys = [key for key in device.keys if key.key_type == "wpkh"] + source_wallet = wallet_manager.create_wallet(wallet_id, 1, "wpkh", keys, [device]) + return source_wallet + + +@pytest.fixture +def unfunded_hot_wallet_1(specter_regtest_configured, hot_wallet_device_1) -> Wallet: + return create_hot_wallet_with_ID( + specter_regtest_configured, + hot_wallet_device_1, + f"a_hotwallet_{random.randint(0, 999999)}", + ) + + +@pytest.fixture +def unfunded_hot_wallet_2(specter_regtest_configured, hot_wallet_device_1) -> Wallet: + return create_hot_wallet_with_ID( + specter_regtest_configured, + hot_wallet_device_1, + f"a_hotwallet_{random.randint(0, 999999)}", + ) + + +@pytest.fixture +def funded_hot_wallet_1( + bitcoin_regtest: NodeController, unfunded_hot_wallet_1: Wallet +) -> Wallet: + funded_hot_wallet_1 = unfunded_hot_wallet_1 + assert len(funded_hot_wallet_1.txlist()) == 0 + for i in range(0, 10): + bitcoin_regtest.testcoin_faucet(funded_hot_wallet_1.getnewaddress(), amount=1) + funded_hot_wallet_1.update() + for i in range(0, 2): + bitcoin_regtest.testcoin_faucet( + funded_hot_wallet_1.getnewaddress(), + amount=2.5, + confirm_payment=False, + ) + time.sleep(1) # needed for tx to propagate + funded_hot_wallet_1.update() + # 12 txs + assert len(funded_hot_wallet_1.txlist()) == 12 + # two of them are unconfirmed + assert ( + len([tx for tx in funded_hot_wallet_1.txlist() if tx["confirmations"] == 0]) + == 2 + ) + return funded_hot_wallet_1 + + +@pytest.fixture +def funded_hot_wallet_2( + bitcoin_regtest: NodeController, unfunded_hot_wallet_2: Wallet +) -> Wallet: + funded_hot_wallet_2 = unfunded_hot_wallet_2 + bitcoin_regtest.testcoin_faucet(funded_hot_wallet_2.getnewaddress()) + return funded_hot_wallet_2 \ No newline at end of file diff --git a/spotbit-plugin/tests/fix_ghost_machine.py b/spotbit-plugin/tests/fix_ghost_machine.py new file mode 100644 index 0000000..99c8df4 --- /dev/null +++ b/spotbit-plugin/tests/fix_ghost_machine.py @@ -0,0 +1,67 @@ +import pytest + +# Using https://iancoleman.io/bip39/ and https://jlopp.github.io/xpub-converter/ +# mnemonic = "ghost ghost ghost ghost ghost ghost ghost ghost ghost ghost ghost machine" + + +# m/44'/0'/0' + + +@pytest.fixture +def ghost_machine_xpub_44(): + xpub = "xpub6CGap5qbgNCEsvXg2gAjEho17zECMA9PbZa7QkrEWTPnPRaubE6qKots5pNwhyFtuYSPa9gQu4jTTZi8WPaXJhtCHrvHQaFRqayN1saQoWv" + return xpub + + +# m/49'/0'/0' + + +@pytest.fixture +def ghost_machine_xpub_49(): + xpub = "xpub6BtcNhqbaFaoC3oEfKky3Sm22pF48U2jmAf78cB3wdAkkGyAgmsVrgyt1ooSt3bHWgzsdUQh2pTJ867yTeUAMmFDKNSBp8J7WPmp7Df7zjv" + return xpub + + +@pytest.fixture +def ghost_machine_ypub(): + ypub = "ypub6WisgNWWiw8H3LzMVgYbFXrXCnPW562EgHBKv14wKdYdoNnPwS34Uke231m2sxFCvL7gNx1FVUor1NjYBLtB9zvpBi8cQ37bn7qTVqo3fjR" + return ypub + + +@pytest.fixture +def ghost_machine_tpub_49(): + tpub = "tpubDC5CZBbVc15fpTeqkyUBKgHqYCqkeaUtPjvGz7RJEttndfcN29psPcxTSj5RNJaWYaRQq8kqovLBrZA2tju3ThSAP9fY1eiSvorchnseFZu" + return tpub + + +@pytest.fixture +def ghost_machine_upub(): + upub = "upub5DCn7wm4SgVmzmtdoi8DVVfxhBJkqL1L6mmKHNgVky1Fj5VyBxV6NzKD957sr5fWXkY5y8THtqSVWWpjLnomBYw4iXpxaPbkXg5Gn6s5tQf" + return upub + + +# m/84'/0'/0' + + +@pytest.fixture +def ghost_machine_xpub_84(): + xpub = "xpub6CjsHfiuBnHMPBkxThQ4DDjTw2Qq3VMEVcPBoMBGejZGkj3WQR15LeJLmymPpSzYHX21C8SdFWHgMw2RUBdAQ2Aj4MMS93a68mxPQeS8oHr" + return xpub + + +@pytest.fixture +def ghost_machine_zpub(): + zpub = "zpub6rQPu14jV9NK5n9C8QyJdPvUGxhivjLEKqRdN8y3QkK2rvfxujLCamccpPgZpGJP6oFch5dkApzn8WFYuaTBzVXvo2kHJsD4gE5gBnCBYj1" + return zpub + + +@pytest.fixture +def ghost_machine_tpub_84(): + tpub = "tpubDC4DsqH5rqHqipMNqUbDFtQT3AkKkUrvLsN6miySvortU3s1LGaNVAb7wX2No2VsuxQV82T8s3HJLv3kdx1CPjsJ3onC1Zo5mWCQzRVaWVX" + return tpub + + +@pytest.fixture +def ghost_machine_vpub(): + vpub = "vpub5Y24kG7ZrCFRkRnHia2sdnt5N7MmsrNry1jMrP8XptMEcZZqkjQA6bc1f52RGiEoJmdy1Vk9Qck9tAL1ohKvuq3oFXe3ADVse6UiTHzuyKx" + return vpub \ No newline at end of file diff --git a/spotbit-plugin/tests/fix_keys_and_seeds.py b/spotbit-plugin/tests/fix_keys_and_seeds.py new file mode 100644 index 0000000..de08d05 --- /dev/null +++ b/spotbit-plugin/tests/fix_keys_and_seeds.py @@ -0,0 +1,138 @@ +from binascii import hexlify +import json +import pytest +from cryptoadvance.specter.key import Key + +from embit.bip39 import mnemonic_to_seed +from embit.bip32 import HDKey, NETWORKS +from embit import script + +mnemonic_ghost_machine = ( + "ghost ghost ghost ghost ghost ghost ghost ghost ghost ghost ghost machine" +) + +mnemonic_zoo_when = ( + "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when" +) + + +@pytest.fixture +def mnemonic_keen_join(): + return 11 * "keen " + "join" + + +# hold hold hold hold hold hold hold hold hold hold hold accident +# This is a formal creation of all major bitcoin artifacts from the +# hold accident mnemonic + + +@pytest.fixture +def mnemonic_hold_accident(): + return 11 * "hold " + "accident" + + +@pytest.fixture +def seed_hold_accident(mnemonic_hold_accident): + seed = mnemonic_to_seed(mnemonic_hold_accident) + print(f"Hold Accident seed: {hexlify(seed)}") + return mnemonic_to_seed(mnemonic_hold_accident) + + +@pytest.fixture +def rootkey_hold_accident(seed_hold_accident): + rootkey = HDKey.from_seed(seed_hold_accident) + print(f"Hold Accident rootkey: {rootkey.to_base58()}") + # xprv9s21ZrQH143K45uYUg7zhHku3bik5a2nw8XcanYCUGHn7RE1Bhkr53RWcjAQVFDTmruDceNDAGbc7yYsZCGveKMDrPr18hMsMcvYTGJ4Mae + print(f"Hold Accident rootkey fp: {hexlify(rootkey.my_fingerprint)}") + return rootkey + + +@pytest.fixture +def acc0xprv_hold_accident(rootkey_hold_accident: HDKey): + xprv = rootkey_hold_accident.derive("m/84h/1h/0h") + print(f"Hold Accident acc0xprv: {xprv.to_base58(version=NETWORKS['test']['xprv'])}") + # tprv8g6WHqYgjvGrEU6eEdJxXzNUqN8DvLFb3iv3yUVomNRcNqT5JSKpTVNBzBD3qTDmmhRHPLcjE5fxFcGmU3FqU5u9zHm9W6sGX2isPMZAKq2 + + return xprv + + +@pytest.fixture +def acc0xpub_hold_accident(acc0xprv_hold_accident: HDKey): + xpub = acc0xprv_hold_accident.to_public() + print(f"Hold Accident acc0xpub: {xpub.to_base58(version=NETWORKS['test']['xpub'])}") + # vpub5YkPJgRQsev79YZM1NRDKJWDjLFcD2xSFAt6LehC5iiMMqQgMHyCFQzwsu16Rx9rBpXZVXPjWAxybuCpsayaw8qCDZtjwH9vifJ7WiQkHwu + return xpub + + +@pytest.fixture +def acc0key0pubkey_hold_accident(acc0xpub_hold_accident: HDKey): + pubkey = acc0xpub_hold_accident.derive("m/0/0") + print("------------") + print(pubkey.key) + # 03584dc8282f626ce5570633018be0760baae68f1ecd6e801192c466ada55f5f31 + print(hexlify(pubkey.sec())) + # b'03584dc8282f626ce5570633018be0760baae68f1ecd6e801192c466ada55f5f31' + return pubkey + + +@pytest.fixture +def acc0key0addr_hold_accident(acc0key0pubkey_hold_accident): + sc = script.p2wpkh(acc0key0pubkey_hold_accident) + address = sc.address(NETWORKS["test"]) + print(address) # m/84'/1'/0'/0/0 + # tb1qnwc84tkupy5v0tzgt27zkd3uxex3nmyr6vfhdd + return address + + +@pytest.fixture +def key_hold_accident(acc0key0pubkey_hold_accident): + sc = script.p2wpkh(acc0key0pubkey_hold_accident) + address = sc.address(NETWORKS["test"]) + print(address) # m/84'/1'/0'/0/0 + # tb1qnwc84tkupy5v0tzgt27zkd3uxex3nmyr6vfhdd + return address + + +@pytest.fixture +def acc0key_hold_accident(acc0xpub_hold_accident, rootkey_hold_accident: HDKey): + + key: Key = Key( + acc0xpub_hold_accident.to_base58( + version=NETWORKS["test"]["xpub"] + ), # original (ToDo: better original) + hexlify(rootkey_hold_accident.my_fingerprint).decode("utf-8"), # fingerprint + "m/84h/1h/0h", # derivation + "wpkh", # key_type + "Muuh", # purpose + acc0xpub_hold_accident.to_base58(version=NETWORKS["test"]["xpub"]), # xpub + ) + mydict = key.json + print(json.dumps(mydict)) + + return key + + +# random other keys + + +@pytest.fixture +def a_key(): + a_key = Key( + "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", + "08686ac6", + "m/48h/1h/0h/2h", + "wsh", + "", + "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL", + ) + return a_key + + +@pytest.fixture +def a_tpub_only_key(): + a_tpub_only_key = Key.from_json( + { + "original": "tpubDDZ5jjGT5RvrAyjoLZfdCfv1PAPmicnhNctwZGKiCMF1Zy5hCGMqppxwYZzWgvPqk7LucMMHo7rkB6Dyj5ZLd2W62FAEP3U6pV4jD5gb9ma" + } + ) + return a_tpub_only_key \ No newline at end of file diff --git a/spotbit-plugin/tests/fix_testnet.py b/spotbit-plugin/tests/fix_testnet.py new file mode 100644 index 0000000..e0e767e --- /dev/null +++ b/spotbit-plugin/tests/fix_testnet.py @@ -0,0 +1,59 @@ +""" A set of fixtures which assume a testnet-node on localhost. This can be helpfull while developing. + +""" + +from cryptoadvance.specter.user import User, hash_password +import pytest +from cryptoadvance.specter.specter import Specter +from cryptoadvance.specter.user import User + + +@pytest.fixture +def specter_testnet_configured(bitcoin_regtest, devices_filled_data_folder): + """This assumes a bitcoin-testnet-node is running on loalhost""" + + config = { + "rpc": { + "autodetect": False, + "datadir": "", + "user": "bitcoin", # change this to your credential in bitcoin.conf (for testnet) + "password": "secret", + "port": 18332, + "host": "localhost", + "protocol": "http", + }, + "auth": { + "method": "rpcpasswordaspin", + }, + } + specter = Specter(data_folder=devices_filled_data_folder, config=config) + specter.check() + assert specter.chain == "test" + + # Create a User + someuser = specter.user_manager.add_user( + User.from_json( + user_dict={ + "id": "someuser", + "username": "someuser", + "password": hash_password("somepassword"), + "config": {}, + "is_admin": False, + "services": None, + }, + specter=specter, + ) + ) + specter.user_manager.save() + specter.check() + + assert not specter.wallet_manager.working_folder is None + try: + yield specter + finally: + # Deleting all Wallets (this will also purge them on core) + for user in specter.user_manager.users: + for wallet in list(user.wallet_manager.wallets.values()): + user.wallet_manager.delete_wallet( + wallet, bitcoin_datadir=bitcoin_regtest.datadir, chain="regtest" + ) \ No newline at end of file