diff --git a/pyinstaller/hooks/hook-hwilib.devices.py b/pyinstaller/hooks/hook-hwilib.devices.py index be7bcec558..055e38e005 100644 --- a/pyinstaller/hooks/hook-hwilib.devices.py +++ b/pyinstaller/hooks/hook-hwilib.devices.py @@ -1,4 +1,5 @@ from hwilib.devices import __all__ + hiddenimports = [] for d in __all__: - hiddenimports.append('hwilib.devices.' + d) \ No newline at end of file + hiddenimports.append("hwilib.devices." + d) diff --git a/pyinstaller/hwibridge.py b/pyinstaller/hwibridge.py index e9d0e201cc..745ac8e803 100644 --- a/pyinstaller/hwibridge.py +++ b/pyinstaller/hwibridge.py @@ -5,21 +5,24 @@ if __name__ == "__main__": # central and early configuring of logging see # https://flask.palletsprojects.com/en/1.1.x/logging/#basic-configuration - dictConfig({ - 'version': 1, - 'formatters': {'default': { - 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', - }}, - 'handlers': {'wsgi': { - 'class': 'logging.StreamHandler', - 'stream': 'ext://flask.logging.wsgi_errors_stream', - 'formatter': 'default' - }}, - 'root': { - 'level': 'INFO', - 'handlers': ['wsgi'] + dictConfig( + { + "version": 1, + "formatters": { + "default": { + "format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s", + } + }, + "handlers": { + "wsgi": { + "class": "logging.StreamHandler", + "stream": "ext://flask.logging.wsgi_errors_stream", + "formatter": "default", + } + }, + "root": {"level": "INFO", "handlers": ["wsgi"]}, } - }) + ) if "--daemon" in sys.argv: print("Daemon mode is not supported in binaries yet") sys.exit(1) diff --git a/pyinstaller/specter_desktop.py b/pyinstaller/specter_desktop.py index 127efbf7ef..56ce15b7ab 100644 --- a/pyinstaller/specter_desktop.py +++ b/pyinstaller/specter_desktop.py @@ -1,9 +1,31 @@ from PyQt5.QtGui import QIcon, QCursor, QDesktopServices -from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMenu, QAction, \ - QDialog, QDialogButtonBox, QVBoxLayout, QRadioButton, QLineEdit, \ - QFileDialog, QLabel, QWidget -from PyQt5.QtCore import QRunnable, QThreadPool, QSettings, QUrl, \ - Qt, pyqtSignal, pyqtSlot, QObject, QSize, QPoint, QEvent +from PyQt5.QtWidgets import ( + QApplication, + QSystemTrayIcon, + QMenu, + QAction, + QDialog, + QDialogButtonBox, + QVBoxLayout, + QRadioButton, + QLineEdit, + QFileDialog, + QLabel, + QWidget, +) +from PyQt5.QtCore import ( + QRunnable, + QThreadPool, + QSettings, + QUrl, + Qt, + pyqtSignal, + pyqtSlot, + QObject, + QSize, + QPoint, + QEvent, +) from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage import sys @@ -24,9 +46,10 @@ path = os.path.dirname(os.path.abspath(__file__)) is_specterd_running = False specterd_thread = None -settings = QSettings('cryptoadvance', 'specter') +settings = QSettings("cryptoadvance", "specter") wait_for_specterd_process = None + def resource_path(relative_path): try: base_path = sys._MEIPASS @@ -36,7 +59,6 @@ def resource_path(relative_path): class SpecterPreferencesDialog(QDialog): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -60,30 +82,23 @@ def __init__(self, *args, **kwargs): placeholderText="Please enter the remote Specter URL" ) - is_remote_mode = settings.value( - 'remote_mode', - defaultValue=False, - type=bool - ) + is_remote_mode = settings.value("remote_mode", defaultValue=False, type=bool) if is_remote_mode: self.mode_remote.setChecked(True) else: self.mode_local.setChecked(True) self.specter_url.hide() - settings.setValue('remote_mode_temp', is_remote_mode) - remote_specter_url = settings.value( - 'specter_url', - defaultValue='', - type=str - ) if is_remote_mode else '' - settings.setValue('specter_url_temp', remote_specter_url) + settings.setValue("remote_mode_temp", is_remote_mode) + remote_specter_url = ( + settings.value("specter_url", defaultValue="", type=str) + if is_remote_mode + else "" + ) + settings.setValue("specter_url_temp", remote_specter_url) self.specter_url.setText(remote_specter_url) self.specter_url.textChanged.connect( - lambda: settings.setValue( - 'specter_url_temp', - self.specter_url.text() - ) + lambda: settings.setValue("specter_url_temp", self.specter_url.text()) ) self.layout.addWidget(self.mode_local) @@ -96,19 +111,22 @@ def __init__(self, *args, **kwargs): def toggle_mode(self): if self.mode_local.isChecked(): - settings.setValue('remote_mode_temp', False) + settings.setValue("remote_mode_temp", False) self.specter_url.hide() else: - settings.setValue('remote_mode_temp', True) + settings.setValue("remote_mode_temp", True) self.specter_url.show() + # Cross communication between threads via signals # https://www.learnpyqt.com/courses/concurrent-execution/multithreading-pyqt-applications-qthreadpool/ + class ProcessSignals(QObject): error = pyqtSignal() result = pyqtSignal() + class ProcessRunnable(QRunnable): def __init__(self, menu): super().__init__() @@ -120,25 +138,31 @@ def run(self): menu = self.menu start_specterd_menu = menu.actions()[0] start_specterd_menu.setEnabled(False) - start_specterd_menu.setText('Starting up Specter{} daemon...'.format( - ' HWIBridge' if settings.value( - "remote_mode", defaultValue=False, type=bool - ) else '' - )) + start_specterd_menu.setText( + "Starting up Specter{} daemon...".format( + " HWIBridge" + if settings.value("remote_mode", defaultValue=False, type=bool) + else "" + ) + ) while running: is_remote_mode = settings.value( - "remote_mode", defaultValue=False, type=bool + "remote_mode", defaultValue=False, type=bool ) try: if is_remote_mode: - requests.get("http://localhost:25441/hwi/settings", allow_redirects=False) + requests.get( + "http://localhost:25441/hwi/settings", allow_redirects=False + ) else: requests.get("http://localhost:25441/login", allow_redirects=False) - start_specterd_menu.setText('Specter{} daemon is running'.format( - ' HWIBridge' if settings.value( - "remote_mode", defaultValue=False, type=bool - ) else '' - )) + start_specterd_menu.setText( + "Specter{} daemon is running".format( + " HWIBridge" + if settings.value("remote_mode", defaultValue=False, type=bool) + else "" + ) + ) toggle_specterd_status(menu) self.signals.result.emit() return @@ -149,11 +173,14 @@ def run(self): def start(self): QThreadPool.globalInstance().start(self) + def watch_specterd(menu, view, first_time=False): global specterd_thread, wait_for_specterd_process try: wait_for_specterd_process = ProcessRunnable(menu) - wait_for_specterd_process.signals.result.connect(lambda: open_webview(view, first_time)) + wait_for_specterd_process.signals.result.connect( + lambda: open_webview(view, first_time) + ) wait_for_specterd_process.signals.error.connect(lambda: print("error")) wait_for_specterd_process.start() except Exception as e: @@ -175,11 +202,13 @@ def toggle_specterd_status(menu): open_webview_menu.setEnabled(True) open_browser_menu.setEnabled(True) else: - start_specterd_menu.setText('Start Specter{} daemon'.format( - ' HWIBridge' if settings.value( - "remote_mode", defaultValue=False, type=bool - ) else '' - )) + start_specterd_menu.setText( + "Start Specter{} daemon".format( + " HWIBridge" + if settings.value("remote_mode", defaultValue=False, type=bool) + else "" + ) + ) start_specterd_menu.setEnabled(True) open_webview_menu.setEnabled(False) open_browser_menu.setEnabled(False) @@ -191,71 +220,61 @@ def quit_specter(app): running = False app.quit() + def open_settings(): dlg = SpecterPreferencesDialog() if dlg.exec_(): is_remote_mode = settings.value( - 'remote_mode_temp', - defaultValue=False, - type=bool - ) - settings.setValue( - 'remote_mode', - is_remote_mode + "remote_mode_temp", defaultValue=False, type=bool ) + settings.setValue("remote_mode", is_remote_mode) specter_url_temp = settings.value( - 'specter_url_temp', - defaultValue='http://localhost:25441/', - type=str + "specter_url_temp", defaultValue="http://localhost:25441/", type=str ) if not specter_url_temp.endswith("/"): specter_url_temp += "/" # missing schema? if "://" not in specter_url_temp: - specter_url_temp = "http://"+specter_url_temp + specter_url_temp = "http://" + specter_url_temp settings.setValue( - 'specter_url', - specter_url_temp if is_remote_mode else 'http://localhost:25441/' + "specter_url", + specter_url_temp if is_remote_mode else "http://localhost:25441/", ) hwibridge_settings_path = os.path.join( - os.path.expanduser(DATA_FOLDER), - "hwi_bridge_config.json" + os.path.expanduser(DATA_FOLDER), "hwi_bridge_config.json" ) if is_remote_mode: - config = { - 'whitelisted_domains': 'http://127.0.0.1:25441/' - } + config = {"whitelisted_domains": "http://127.0.0.1:25441/"} if os.path.isfile(hwibridge_settings_path): with open(hwibridge_settings_path, "r") as f: file_config = json.loads(f.read()) deep_update(config, file_config) with open(hwibridge_settings_path, "w") as f: - if 'whitelisted_domains' in config: - whitelisted_domains = '' - if specter_url_temp not in config[ - 'whitelisted_domains' - ].split(): - config['whitelisted_domains'] += ' ' + specter_url_temp - for url in config['whitelisted_domains'].split(): - if not url.endswith("/") and url != '*': + if "whitelisted_domains" in config: + whitelisted_domains = "" + if specter_url_temp not in config["whitelisted_domains"].split(): + config["whitelisted_domains"] += " " + specter_url_temp + for url in config["whitelisted_domains"].split(): + if not url.endswith("/") and url != "*": # make sure the url end with a "/" url += "/" - whitelisted_domains += url.strip() + '\n' - config['whitelisted_domains'] = whitelisted_domains + whitelisted_domains += url.strip() + "\n" + config["whitelisted_domains"] = whitelisted_domains f.write(json.dumps(config, indent=4)) # TODO: Add PORT setting + def open_webview(view, first_time=False): url = settings.value("specter_url", type=str).strip("/") - if first_time and settings.value('remote_mode', defaultValue=False, type=bool): + if first_time and settings.value("remote_mode", defaultValue=False, type=bool): url += "/settings/hwi" # missing schema? if "://" not in url: - url = "http://"+url + url = "http://" + url # if https:// or .onion - use browser if "https://" in url or ".onion" in url: webbrowser.open(settings.value("specter_url", type=str), new=1) @@ -270,8 +289,10 @@ def open_webview(view, first_time=False): getattr(view, "raise")() view.activateWindow() + class WebEnginePage(QWebEnginePage): """Web page""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.featurePermissionRequested.connect(self.onFeaturePermissionRequested) @@ -285,10 +306,9 @@ def onFeaturePermissionRequested(self, url, feature): def onDownloadRequest(self, item): """Catch dowload files requests""" options = QFileDialog.Options() - path = QFileDialog.getSaveFileName(None, - "Where to save?", - item.path(), - options=options)[0] + path = QFileDialog.getSaveFileName( + None, "Where to save?", item.path(), options=options + )[0] if path: item.setPath(path) item.accept() @@ -307,8 +327,10 @@ def open_browser(self, url): QDesktopServices.openUrl(url) page.deleteLater() + class WebView(QWidget): """Window with the web browser""" + def __init__(self, tray, *args, **kwargs): super().__init__(*args, **kwargs) self.setStyleSheet("background-color:#263044;") @@ -324,7 +346,7 @@ def __init__(self, tray, *args, **kwargs): vbox.addWidget(self.progress, stretch=0) vbox.addWidget(self.browser) vbox.setSpacing(0) - vbox.setContentsMargins(0,0,0,0) + vbox.setContentsMargins(0, 0, 0, 0) self.setLayout(vbox) self.resize(settings.value("size", QSize(1200, 900))) self.move(settings.value("pos", QPoint(50, 50))) @@ -343,7 +365,7 @@ def loadStartedHandler(self): def loadProgressHandler(self, progress): # just changes opacity over time for now - alpha = int(time.time()*100)%100 + alpha = int(time.time() * 100) % 100 self.progress.setStyleSheet(f"background-color:rgba(75,140,26,{alpha});") def loadFinishedHandler(self, *args, **kwargs): @@ -360,13 +382,16 @@ def closeEvent(self, *args, **kwargs): settings.setValue("size", self.size()) settings.setValue("pos", self.pos()) - if settings.value('first_time_close', defaultValue=True, type=bool): - settings.setValue('first_time_close', False) - self.tray.showMessage("Specter is still running!", - "Use tray icon to quit or reopen", - self.tray.icon()) + if settings.value("first_time_close", defaultValue=True, type=bool): + settings.setValue("first_time_close", False) + self.tray.showMessage( + "Specter is still running!", + "Use tray icon to quit or reopen", + self.tray.icon(), + ) super().closeEvent(*args, **kwargs) + class Application(QApplication): def event(self, event): # not sure what 20 means @@ -374,6 +399,7 @@ def event(self, event): quit_specter(self) return False + def init_desktop_app(): app = Application([]) app.setQuitOnLastWindowClosed(False) @@ -392,12 +418,9 @@ def sigint_handler(*args): # print("-----------------------------------------------------------------------") # print(psutil.Process().memory_maps()) # print("-----------------------------------------------------------------------") - + # Create the icon - icon = QIcon(os.path.join( - resource_path('icons'), - 'icon.png' - )) + icon = QIcon(os.path.join(resource_path("icons"), "icon.png")) # Create the tray tray = QSystemTrayIcon() tray.setIcon(icon) @@ -408,11 +431,13 @@ def sigint_handler(*args): # Create the menu menu = QMenu() - start_specterd_menu = QAction("Start Specter{} daemon".format( - ' HWIBridge' if settings.value( - "remote_mode", defaultValue=False, type=bool - ) else '' - )) + start_specterd_menu = QAction( + "Start Specter{} daemon".format( + " HWIBridge" + if settings.value("remote_mode", defaultValue=False, type=bool) + else "" + ) + ) start_specterd_menu.triggered.connect(lambda: watch_specterd(menu, view)) menu.addAction(start_specterd_menu) @@ -441,11 +466,11 @@ def sigint_handler(*args): app.setWindowIcon(icon) # Setup settings - first_time = settings.value('first_time', defaultValue=True, type=bool) + first_time = settings.value("first_time", defaultValue=True, type=bool) if first_time: - settings.setValue('first_time', False) - settings.setValue('remote_mode', False) - settings.setValue('specter_url', "http://localhost:25441/") + settings.setValue("first_time", False) + settings.setValue("remote_mode", False) + settings.setValue("specter_url", "http://localhost:25441/") open_settings() # start server diff --git a/pyinstaller/specterd.py b/pyinstaller/specterd.py index 4917e771f3..2c818380ca 100644 --- a/pyinstaller/specterd.py +++ b/pyinstaller/specterd.py @@ -6,15 +6,17 @@ if __name__ == "__main__": # central and early configuring of logging see # https://flask.palletsprojects.com/en/1.1.x/logging/#basic-configuration - + ch = logging.StreamHandler() ch.setLevel(logging.INFO) - formatter = logging.Formatter('[%(asctime)s] %(levelname)s in %(module)s: %(message)s') + formatter = logging.Formatter( + "[%(asctime)s] %(levelname)s in %(module)s: %(message)s" + ) ch.setFormatter(formatter) logging.getLogger().addHandler(ch) logging.getLogger().setLevel(logging.INFO) logging.getLogger(__name__).info("Logging configured") - + if "--daemon" in sys.argv: print("Daemon mode is not supported in binaries yet") sys.exit(1) diff --git a/setup.py b/setup.py index e403898cca..c95332f33a 100644 --- a/setup.py +++ b/setup.py @@ -2,11 +2,11 @@ from setuptools import find_packages, setup -with open('requirements.txt') as f: - install_reqs = f.read().strip().split('\n') +with open("requirements.txt") as f: + install_reqs = f.read().strip().split("\n") -reqs = [str(ir) for ir in install_reqs if not ir.startswith("#") ] +reqs = [str(ir) for ir in install_reqs if not ir.startswith("#")] with open("README.md", "r") as fh: @@ -21,8 +21,8 @@ long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/cryptoadvance/specter-desktop", - packages=find_packages('src'), - package_dir={'': 'src'}, + packages=find_packages("src"), + package_dir={"": "src"}, # take METADATA.in into account, include that stuff as well (static/templates) include_package_data=True, install_requires=reqs, @@ -32,5 +32,5 @@ "Operating System :: OS Independent", "Framework :: Flask", ], - python_requires='>=3.6', + python_requires=">=3.6", ) diff --git a/src/cryptoadvance/specter/__main__.py b/src/cryptoadvance/specter/__main__.py index 810e1debb7..c525ce38e2 100644 --- a/src/cryptoadvance/specter/__main__.py +++ b/src/cryptoadvance/specter/__main__.py @@ -8,7 +8,9 @@ # However the dictConfig doesn't work, so let's do something similiar programatically ch = logging.StreamHandler() ch.setLevel(logging.INFO) - formatter = logging.Formatter('[%(asctime)s] %(levelname)s in %(module)s: %(message)s') + formatter = logging.Formatter( + "[%(asctime)s] %(levelname)s in %(module)s: %(message)s" + ) ch.setFormatter(formatter) logging.getLogger().addHandler(ch) logging.getLogger().setLevel(logging.INFO) diff --git a/src/cryptoadvance/specter/bitcoind.py b/src/cryptoadvance/specter/bitcoind.py index 54d268e7cf..52c41617c7 100644 --- a/src/cryptoadvance/specter/bitcoind.py +++ b/src/cryptoadvance/specter/bitcoind.py @@ -1,6 +1,6 @@ -''' Stuff to control a bitcoind-instance. Either directly by access to a bitcoind-executable or +""" Stuff to control a bitcoind-instance. Either directly by access to a bitcoind-executable or via docker. -''' +""" import atexit import logging import os @@ -19,9 +19,13 @@ logger = logging.getLogger(__name__) + class Btcd_conn: - ''' An object to easily store connection data ''' - def __init__(self, rpcuser="bitcoin", rpcpassword="secret", rpcport=18543, ipaddress=None): + """An object to easily store connection data""" + + def __init__( + self, rpcuser="bitcoin", rpcpassword="secret", rpcport=18543, ipaddress=None + ): self.rpcport = rpcport self.rpcuser = rpcuser self.rpcpassword = rpcpassword @@ -34,35 +38,40 @@ def ipaddress(self): else: return self._ipaddress - @ipaddress.setter - def ipaddress(self,ipaddress): + def ipaddress(self, ipaddress): self._ipaddress = ipaddress def get_rpc(self): - ''' returns a BitcoinRPC ''' + """returns a BitcoinRPC""" # def __init__(self, user, passwd, host="127.0.0.1", port=8332, protocol="http", path="", timeout=30, **kwargs): - rpc = BitcoinRPC(self.rpcuser, self.rpcpassword, host=self.ipaddress, port=self.rpcport) + rpc = BitcoinRPC( + self.rpcuser, self.rpcpassword, host=self.ipaddress, port=self.rpcport + ) rpc.getblockchaininfo() return rpc def render_url(self): - return 'http://{}:{}@{}:{}/wallet/'.format(self.rpcuser, self.rpcpassword, self.ipaddress, self.rpcport) - + return "http://{}:{}@{}:{}/wallet/".format( + self.rpcuser, self.rpcpassword, self.ipaddress, self.rpcport + ) + def __repr__(self): - return ''.format(self.render_url()) + return "".format(self.render_url()) + class BitcoindController: - ''' A kind of abstract class to simplify running a bitcoind with or without docker ''' + """A kind of abstract class to simplify running a bitcoind with or without docker""" + def __init__(self, rpcport=18443): self.rpcconn = Btcd_conn(rpcport=rpcport) def start_bitcoind(self, cleanup_at_exit=False): - ''' starts bitcoind with a specific rpcport=18543 by default. - That's not the standard in order to make pytest running while - developing locally against a different regtest-instance - if bitcoind_path == docker, it'll run bitcoind via docker - ''' + """starts bitcoind with a specific rpcport=18543 by default. + That's not the standard in order to make pytest running while + developing locally against a different regtest-instance + if bitcoind_path == docker, it'll run bitcoind via docker + """ if self.check_existing() != None: return self.check_existing() @@ -73,15 +82,14 @@ def start_bitcoind(self, cleanup_at_exit=False): self.mine(block_count=100) return self.rpcconn - def version(self): - ''' Returns the version of bitcoind, e.g. "v0.19.1" ''' - version = self.get_rpc().getnetworkinfo()['subversion'] - version = version.replace('/','').replace('Satoshi:','v') + '''Returns the version of bitcoind, e.g. "v0.19.1"''' + version = self.get_rpc().getnetworkinfo()["subversion"] + version = version.replace("/", "").replace("Satoshi:", "v") return version def get_rpc(self): - ''' wrapper for convenience ''' + """wrapper for convenience""" return self.rpcconn.get_rpc() def _start_bitcoind(self, cleanup_at_exit): @@ -94,11 +102,11 @@ def stop_bitcoind(self): raise Exception("This should not be used in the baseclass!") def mine(self, address="mruae2834buqxk77oaVpephnA5ZAxNNJ1r", block_count=1): - ''' Does mining to the attached address with as many as block_count blocks ''' + """Does mining to the attached address with as many as block_count blocks""" self.rpcconn.get_rpc().generatetoaddress(block_count, address) - + def testcoin_faucet(self, address, amount=20, mine_tx=False): - ''' an easy way to get some testcoins ''' + """an easy way to get some testcoins""" rpc = self.get_rpc() try: test3rdparty_rpc = rpc.wallet("test3rdparty") @@ -106,7 +114,7 @@ def testcoin_faucet(self, address, amount=20, mine_tx=False): except RpcError as rpce: # return-codes: # https://github.com/bitcoin/bitcoin/blob/v0.15.0.1/src/rpc/protocol.h#L32L87 - if rpce.error_code == -18: # RPC_WALLET_NOT_FOUND + if rpce.error_code == -18: # RPC_WALLET_NOT_FOUND logger.debug("Creating test3rdparty wallet") rpc.createwallet("test3rdparty") test3rdparty_rpc = rpc.wallet("test3rdparty") @@ -116,15 +124,15 @@ def testcoin_faucet(self, address, amount=20, mine_tx=False): if balance < amount: test3rdparty_address = test3rdparty_rpc.getnewaddress("test3rdparty") rpc.generatetoaddress(102, test3rdparty_address) - test3rdparty_rpc.sendtoaddress(address,amount) + test3rdparty_rpc.sendtoaddress(address, amount) if mine_tx: rpc.generatetoaddress(1, test3rdparty_address) - + @staticmethod def check_bitcoind(rpcconn): - ''' returns true if bitcoind is running on that address/port ''' + """returns true if bitcoind is running on that address/port""" try: - rpcconn.get_rpc() # that call will also check the connection + rpcconn.get_rpc() # that call will also check the connection return True except ConnectionRefusedError: return False @@ -135,7 +143,7 @@ def check_bitcoind(rpcconn): @staticmethod def wait_for_bitcoind(rpcconn): - ''' tries to reach the bitcoind via rpc. Will timeout after 10 seconds ''' + """tries to reach the bitcoind via rpc. Will timeout after 10 seconds""" i = 0 while True: if BitcoindController.check_bitcoind(rpcconn): @@ -143,175 +151,251 @@ def wait_for_bitcoind(rpcconn): time.sleep(0.5) i = i + 1 if i > 20: - raise Exception("Timeout while trying to reach bitcoind at rpcport {} !".format(rpcconn)) - + raise Exception( + "Timeout while trying to reach bitcoind at rpcport {} !".format( + rpcconn + ) + ) + @staticmethod def render_rpc_options(rpcconn): - options = " -rpcport={} -rpcuser={} -rpcpassword={} ".format(rpcconn.rpcport, rpcconn.rpcuser,rpcconn.rpcpassword) + options = " -rpcport={} -rpcuser={} -rpcpassword={} ".format( + rpcconn.rpcport, rpcconn.rpcuser, rpcconn.rpcpassword + ) return options - @classmethod - def construct_bitcoind_cmd(cls, rpcconn, run_docker=True,datadir=None, bitcoind_path='bitcoind'): - ''' returns a bitcoind-command to run bitcoind ''' + def construct_bitcoind_cmd( + cls, rpcconn, run_docker=True, datadir=None, bitcoind_path="bitcoind" + ): + """returns a bitcoind-command to run bitcoind""" btcd_cmd = "{} ".format(bitcoind_path) btcd_cmd += " -regtest " btcd_cmd += " -fallbackfee=0.0002 " - btcd_cmd += " -port={} -rpcport={} -rpcbind=0.0.0.0 -rpcbind=0.0.0.0".format(rpcconn.rpcport-1,rpcconn.rpcport) - btcd_cmd += " -rpcuser={} -rpcpassword={} ".format(rpcconn.rpcuser,rpcconn.rpcpassword) + btcd_cmd += " -port={} -rpcport={} -rpcbind=0.0.0.0 -rpcbind=0.0.0.0".format( + rpcconn.rpcport - 1, rpcconn.rpcport + ) + btcd_cmd += " -rpcuser={} -rpcpassword={} ".format( + rpcconn.rpcuser, rpcconn.rpcpassword + ) btcd_cmd += " -rpcallowip=0.0.0.0/0 -rpcallowip=172.17.0.0/16 " if not run_docker: btcd_cmd += " -noprinttoconsole" if datadir == None: datadir = tempfile.mkdtemp(prefix="bitcoind_datadir") btcd_cmd += " -datadir={} ".format(datadir) - logger.debug("constructed bitcoind-command: %s",btcd_cmd) + logger.debug("constructed bitcoind-command: %s", btcd_cmd) return btcd_cmd + class BitcoindPlainController(BitcoindController): - ''' A class controlling the bicoind-process directly on the machine ''' - def __init__(self, bitcoind_path='bitcoind', rpcport=18443): + """A class controlling the bicoind-process directly on the machine""" + + def __init__(self, bitcoind_path="bitcoind", rpcport=18443): super().__init__(rpcport=rpcport) self.bitcoind_path = bitcoind_path - self.rpcconn.ipaddress = 'localhost' + self.rpcconn.ipaddress = "localhost" def _start_bitcoind(self, cleanup_at_exit=False): datadir = tempfile.mkdtemp(prefix="bitcoind_plain_datadir") - bitcoind_cmd = self.construct_bitcoind_cmd(self.rpcconn, run_docker=False, datadir=datadir, bitcoind_path=self.bitcoind_path) + bitcoind_cmd = self.construct_bitcoind_cmd( + self.rpcconn, + run_docker=False, + datadir=datadir, + bitcoind_path=self.bitcoind_path, + ) logger.debug("About to execute: {}".format(bitcoind_cmd)) # exec will prevent creating a child-process and will make bitcoind_proc.terminate() work as expected - self.bitcoind_proc = subprocess.Popen("exec " + bitcoind_cmd, shell=True) - logger.debug("Running bitcoind-process with pid {}".format(self.bitcoind_proc.pid)) + self.bitcoind_proc = subprocess.Popen("exec " + bitcoind_cmd, shell=True) + logger.debug( + "Running bitcoind-process with pid {}".format(self.bitcoind_proc.pid) + ) + def cleanup_bitcoind(): - self.bitcoind_proc.kill() # much faster then terminate() and speed is key here over being nice - logger.debug("Killed bitcoind-process with pid {}".format(self.bitcoind_proc.pid)) + self.bitcoind_proc.kill() # much faster then terminate() and speed is key here over being nice + logger.debug( + "Killed bitcoind-process with pid {}".format(self.bitcoind_proc.pid) + ) shutil.rmtree(datadir) logger.debug("removed temp-dir") + if cleanup_at_exit: atexit.register(cleanup_bitcoind) - + def stop_bitcoind(self): # not necessary as the cleanup_bitcoind() will do it automatically! # ToDo: Implement it nevertheless pass - + def check_existing(self): - ''' other then in docker, we won't check on the "instance-level". This will return true if if a - bitcoind is running on the default port. - ''' + """other then in docker, we won't check on the "instance-level". This will return true if if a + bitcoind is running on the default port. + """ if not self.check_bitcoind(self.rpcconn): return None else: return True + class BitcoindDockerController(BitcoindController): - ''' A class specifically controlling a docker-based bitcoind-container ''' - def __init__(self,rpcport=18443, docker_tag="latest"): + """A class specifically controlling a docker-based bitcoind-container""" + + def __init__(self, rpcport=18443, docker_tag="latest"): self.btcd_container = None super().__init__(rpcport=rpcport) - self.docker_exec = which('docker') - self.docker_tag=docker_tag + self.docker_exec = which("docker") + self.docker_tag = docker_tag if self.docker_exec == None: - raise("Docker not existing!") + raise ("Docker not existing!") if self.detect_bitcoind_container(rpcport) != None: rpcconn, self.btcd_container = self.detect_bitcoind_container(rpcport) self.rpcconn = rpcconn def _start_bitcoind(self, cleanup_at_exit): - bitcoind_path = self.construct_bitcoind_cmd(self.rpcconn ) + bitcoind_path = self.construct_bitcoind_cmd(self.rpcconn) dclient = docker.from_env() logger.debug("Running (in docker): {}".format(bitcoind_path)) - ports={ - '{}/tcp'.format(self.rpcconn.rpcport-1): self.rpcconn.rpcport-1, - '{}/tcp'.format(self.rpcconn.rpcport): self.rpcconn.rpcport + ports = { + "{}/tcp".format(self.rpcconn.rpcport - 1): self.rpcconn.rpcport - 1, + "{}/tcp".format(self.rpcconn.rpcport): self.rpcconn.rpcport, } logger.debug("portmapping: {}".format(ports)) - image = dclient.images.get("registry.gitlab.com/cryptoadvance/specter-desktop/python-bitcoind:{}".format(self.docker_tag)) - self.btcd_container = dclient.containers.run("registry.gitlab.com/cryptoadvance/specter-desktop/python-bitcoind:{}".format(self.docker_tag), bitcoind_path, ports=ports, detach=True) + image = dclient.images.get( + "registry.gitlab.com/cryptoadvance/specter-desktop/python-bitcoind:{}".format( + self.docker_tag + ) + ) + self.btcd_container = dclient.containers.run( + "registry.gitlab.com/cryptoadvance/specter-desktop/python-bitcoind:{}".format( + self.docker_tag + ), + bitcoind_path, + ports=ports, + detach=True, + ) + def cleanup_docker_bitcoind(): self.btcd_container.stop() self.btcd_container.remove() + if cleanup_at_exit: atexit.register(cleanup_docker_bitcoind) - logger.debug("Waiting for container {} to come up".format(self.btcd_container.id)) + logger.debug( + "Waiting for container {} to come up".format(self.btcd_container.id) + ) self.wait_for_container() rpcconn, _ = self.detect_bitcoind_container(self.rpcconn.rpcport) if rpcconn == None: - raise Exception("Couldn't find container or it died already. Check the logs!") + raise Exception( + "Couldn't find container or it died already. Check the logs!" + ) else: self.rpcconn = rpcconn - return - + return def stop_bitcoind(self): if self.btcd_container != None: self.btcd_container.reload() - if self.btcd_container.status == 'running': + if self.btcd_container.status == "running": _, container = self.detect_bitcoind_container(self.rpcconn.rpcport) if container == self.btcd_container: self.btcd_container.stop() logger.info("Stopped btcd_container {}".format(self.btcd_container)) - return - raise Exception('Ambigious Container running') + return + raise Exception("Ambigious Container running") def check_existing(self): - ''' Checks whether self.btcd_container is up2date and not ambigious ''' + """Checks whether self.btcd_container is up2date and not ambigious""" if self.btcd_container != None: self.btcd_container.reload() - if self.btcd_container.status == 'running': - rpcconn, container = self.detect_bitcoind_container(self.rpcconn.rpcport) + if self.btcd_container.status == "running": + rpcconn, container = self.detect_bitcoind_container( + self.rpcconn.rpcport + ) if container == self.btcd_container: return rpcconn - raise Exception('Ambigious Container running') + raise Exception("Ambigious Container running") return None @staticmethod def search_bitcoind_container(all=False): - ''' returns a list of containers which are running bitcoind ''' + """returns a list of containers which are running bitcoind""" d_client = docker.from_env() - return [c for c in d_client.containers.list(all) if (c.attrs['Config'].get('Cmd')or[""])[0]=='bitcoind'] + return [ + c + for c in d_client.containers.list(all) + if (c.attrs["Config"].get("Cmd") or [""])[0] == "bitcoind" + ] @staticmethod def detect_bitcoind_container(with_rpcport): - ''' checks all the containers for a bitcoind one, parses the arguments and initializes - the object accordingly - returns rpcconn, btcd_container - ''' + """checks all the containers for a bitcoind one, parses the arguments and initializes + the object accordingly + returns rpcconn, btcd_container + """ d_client = docker.from_env() potential_btcd_containers = BitcoindDockerController.search_bitcoind_container() if len(potential_btcd_containers) == 0: - logger.debug("could not detect container. Candidates: {}".format(d_client.containers.list())) - all_candidates = BitcoindDockerController.search_bitcoind_container(all=True) - logger.debug("could not detect container. All Candidates: {}".format(all_candidates)) + logger.debug( + "could not detect container. Candidates: {}".format( + d_client.containers.list() + ) + ) + all_candidates = BitcoindDockerController.search_bitcoind_container( + all=True + ) + logger.debug( + "could not detect container. All Candidates: {}".format(all_candidates) + ) if len(all_candidates) > 0: logger.debug("100 chars of logs of first candidate") logger.debug(all_candidates[0].logs()[0:100]) return None for btcd_container in potential_btcd_containers: - rpcport = int([arg for arg in btcd_container.attrs['Config']['Cmd'] if 'rpcport' in arg][0].split('=')[1]) + rpcport = int( + [ + arg + for arg in btcd_container.attrs["Config"]["Cmd"] + if "rpcport" in arg + ][0].split("=")[1] + ) if rpcport != with_rpcport: - logger.debug("checking port {} against searched port {}".format(type(rpcport), type(with_rpcport))) + logger.debug( + "checking port {} against searched port {}".format( + type(rpcport), type(with_rpcport) + ) + ) continue - rpcpassword = [arg for arg in btcd_container.attrs['Config']['Cmd'] if 'rpcpassword' in arg][0].split('=')[1] - rpcuser = [arg for arg in btcd_container.attrs['Config']['Cmd'] if 'rpcuser' in arg][0].split('=')[1] - if "CI" in os.environ: # this is a predefined variable in gitlab + rpcpassword = [ + arg + for arg in btcd_container.attrs["Config"]["Cmd"] + if "rpcpassword" in arg + ][0].split("=")[1] + rpcuser = [ + arg for arg in btcd_container.attrs["Config"]["Cmd"] if "rpcuser" in arg + ][0].split("=")[1] + if "CI" in os.environ: # this is a predefined variable in gitlab # This works on Linux (direct docker) and gitlab-CI but not on MAC - ipaddress = btcd_container.attrs['NetworkSettings']['IPAddress'] + ipaddress = btcd_container.attrs["NetworkSettings"]["IPAddress"] else: # This works on most machines but not on gitlab-CI - ipaddress ="127.0.0.1" - rpcconn = Btcd_conn(rpcuser=rpcuser, rpcpassword=rpcpassword, rpcport=rpcport, ipaddress=ipaddress) + ipaddress = "127.0.0.1" + rpcconn = Btcd_conn( + rpcuser=rpcuser, + rpcpassword=rpcpassword, + rpcport=rpcport, + ipaddress=ipaddress, + ) logger.info("detected container {}".format(btcd_container.id)) return rpcconn, btcd_container logger.debug("No matching container found") return None - def wait_for_container(self): - ''' waits for the docker-container to come up. Times out after 10 seconds ''' + """waits for the docker-container to come up. Times out after 10 seconds""" i = 0 while True: - ip_address = self.btcd_container.attrs['NetworkSettings']['IPAddress'] + ip_address = self.btcd_container.attrs["NetworkSettings"]["IPAddress"] if ip_address.startswith("172"): self.rpcconn.ipaddress = ip_address break @@ -323,13 +407,13 @@ def wait_for_container(self): def fetch_wallet_addresses_for_mining(data_folder=None): - ''' parses all the wallet-jsons in the folder (default ~/.specter/wallets/regtest) - and returns an array with the addresses - ''' + """parses all the wallet-jsons in the folder (default ~/.specter/wallets/regtest) + and returns an array with the addresses + """ if data_folder == None: data_folder = os.path.expanduser(DATA_FOLDER) - wallets = load_jsons(data_folder+"/wallets/regtest") - address_array = [ value['address'] for key, value in wallets.items()] + wallets = load_jsons(data_folder + "/wallets/regtest") + address_array = [value["address"] for key, value in wallets.items()] # remove duplicates - address_array = list( dict.fromkeys(address_array) ) + address_array = list(dict.fromkeys(address_array)) return address_array diff --git a/src/cryptoadvance/specter/cli.py b/src/cryptoadvance/specter/cli.py index 4668e04a43..b8d7df1053 100644 --- a/src/cryptoadvance/specter/cli.py +++ b/src/cryptoadvance/specter/cli.py @@ -31,12 +31,10 @@ def cli(): # for https: @click.option("--cert") @click.option("--key") -@click.option('--debug/--no-debug', default=None) -@click.option('--tor', is_flag=True) +@click.option("--debug/--no-debug", default=None) +@click.option("--tor", is_flag=True) @click.option("--hwibridge", is_flag=True) -def server(daemon, stop, restart, force, - port, host, cert, key, - debug, tor, hwibridge): +def server(daemon, stop, restart, force, port, host, cert, key, debug, tor, hwibridge): # create an app to get Specter instance # and it's data folder app = create_app() @@ -61,8 +59,10 @@ def server(daemon, stop, restart, force, pass elif daemon: if not force: - print(f"PID file \"{pid_file}\" already exists. \ - Use --force to overwrite") + print( + f'PID file "{pid_file}" already exists. \ + Use --force to overwrite' + ) return else: os.remove(pid_file) @@ -70,12 +70,12 @@ def server(daemon, stop, restart, force, return else: if stop or restart: - print(f"Can't find PID file \"{pid_file}\"") + print(f'Can\'t find PID file "{pid_file}"') if stop: return # watch templates folder to reload when something changes - extra_dirs = ['templates'] + extra_dirs = ["templates"] extra_files = extra_dirs[:] for extra_dir in extra_dirs: for dirname, dirs, files in os.walk(extra_dir): @@ -86,15 +86,15 @@ def server(daemon, stop, restart, force, # if port is not defined - get it from environment if port is None: - port = int(os.getenv('PORT', 25441)) + port = int(os.getenv("PORT", 25441)) else: port = int(port) # certificates if cert is None: - cert = os.getenv('CERT', None) + cert = os.getenv("CERT", None) if key is None: - key = os.getenv('KEY', None) + key = os.getenv("KEY", None) protocol = "http" kwargs = { @@ -112,8 +112,7 @@ def server(daemon, stop, restart, force, print( " * Running HWI Bridge mode.\n" " * You can configure access to the API " - "at: %s://%s:%d/hwi/settings" - % (protocol, host, port) + "at: %s://%s:%d/hwi/settings" % (protocol, host, port) ) # debug is false by default @@ -124,10 +123,10 @@ def run(debug=debug): app.controller = None try: port = 5000 # default flask port - if 'port' in kwargs: - port = kwargs['port'] + if "port" in kwargs: + port = kwargs["port"] else: - kwargs['port'] = port + kwargs["port"] = port # if we have certificates if "ssl_context" in kwargs: tor_port = 443 @@ -136,17 +135,19 @@ def run(debug=debug): app.port = port app.tor_port = tor_port app.save_tor_address_to = toraddr_file - if debug and (tor or os.getenv('CONNECT_TOR') == 'True'): - print(" * Warning: Cannot use Tor in debug mode. \ - Starting in production mode instead.") + if debug and (tor or os.getenv("CONNECT_TOR") == "True"): + print( + " * Warning: Cannot use Tor in debug mode. \ + Starting in production mode instead." + ) debug = False - if tor or os.getenv('CONNECT_TOR') == 'True': + if tor or os.getenv("CONNECT_TOR") == "True": try: app.tor_enabled = True start_hidden_service(app) except Exception as e: - print(f' * Failed to start Tor hidden service: {e}') - print(' * Continuing process with Tor disabled') + print(f" * Failed to start Tor hidden service: {e}") + print(" * Continuing process with Tor disabled") app.tor_service_id = None app.tor_enabled = False else: @@ -163,30 +164,33 @@ def run(debug=debug): print("Starting server in background...") print(" * Hopefully running on %s://%s:%d/" % (protocol, host, port)) # macOS + python3.7 is buggy - if sys.platform == "darwin" and \ - (sys.version_info.major == 3 and sys.version_info.minor < 8): - print(" * WARNING: --daemon mode might not \ + if sys.platform == "darwin" and ( + sys.version_info.major == 3 and sys.version_info.minor < 8 + ): + print( + " * WARNING: --daemon mode might not \ work properly in python 3.7 and lower \ - on MacOS. Upgrade to python 3.8+") + on MacOS. Upgrade to python 3.8+" + ) from daemonize import Daemonize + d = Daemonize(app="specter", pid=pid_file, action=run) d.start() else: # if not a daemon we can use DEBUG if debug is None: - debug = app.config['DEBUG'] + debug = app.config["DEBUG"] run(debug=debug) @cli.command() -@click.option('--debug/--no-debug', default=False) -@click.option('--mining/--no-mining', default=True) -@click.option('--docker-tag', "docker_tag", default="latest") +@click.option("--debug/--no-debug", default=False) +@click.option("--mining/--no-mining", default=True) +@click.option("--docker-tag", "docker_tag", default="latest") def bitcoind(debug, mining, docker_tag): import docker - from .bitcoind import (BitcoindDockerController, - fetch_wallet_addresses_for_mining) + from .bitcoind import BitcoindDockerController, fetch_wallet_addresses_for_mining logging.getLogger().setLevel(logging.INFO) mining_every_x_seconds = 15 @@ -198,22 +202,27 @@ def bitcoind(debug, mining, docker_tag): my_bitcoind.start_bitcoind() except docker.errors.ImageNotFound: click.echo(f" --> Image with tag {docker_tag} does not exist!") - click.echo(f" --> Try to download first with docker pull \ + click.echo( + f" --> Try to download first with docker pull \ registry.gitlab.com/cryptoadvance/specter-desktop\ - /python-bitcoind:{docker_tag}") + /python-bitcoind:{docker_tag}" + ) sys.exit(1) - tags_of_image = [image.split(":")[-1] - for image in my_bitcoind.btcd_container.image.tags] + tags_of_image = [ + image.split(":")[-1] for image in my_bitcoind.btcd_container.image.tags + ] if docker_tag not in tags_of_image: - click.echo(" --> The running docker container is not \ - the tag you requested!") click.echo( - " --> please stop first with docker stop {}" - .format(my_bitcoind.btcd_container.id) + " --> The running docker container is not \ + the tag you requested!" + ) + click.echo( + " --> please stop first with docker stop {}".format( + my_bitcoind.btcd_container.id + ) ) sys.exit(1) - click.echo(" --> containerImage: %s" % - my_bitcoind.btcd_container.image.tags) + click.echo(" --> containerImage: %s" % my_bitcoind.btcd_container.image.tags) click.echo(" --> url: %s" % my_bitcoind.rpcconn.render_url()) click.echo(" --> user, password: bitcoin, secret") click.echo(" --> host, port: localhost, 18443") @@ -224,8 +233,8 @@ def bitcoind(debug, mining, docker_tag): if mining: click.echo( " --> Now, mining a block every %i seconds. \ - Avoid it via --no-mining" % - mining_every_x_seconds + Avoid it via --no-mining" + % mining_every_x_seconds ) # Get each address some coins try: @@ -255,19 +264,22 @@ def bitcoind(debug, mining, docker_tag): if __name__ == "__main__": # central and early configuring of logging see # https://flask.palletsprojects.com/en/1.1.x/logging/#basic-configuration - dictConfig({ - 'version': 1, - 'formatters': {'default': { - 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', - }}, - 'handlers': {'wsgi': { - 'class': 'logging.StreamHandler', - 'stream': 'ext://flask.logging.wsgi_errors_stream', - 'formatter': 'default' - }}, - 'root': { - 'level': 'INFO', - 'handlers': ['wsgi'] + dictConfig( + { + "version": 1, + "formatters": { + "default": { + "format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s", + } + }, + "handlers": { + "wsgi": { + "class": "logging.StreamHandler", + "stream": "ext://flask.logging.wsgi_errors_stream", + "formatter": "default", + } + }, + "root": {"level": "INFO", "handlers": ["wsgi"]}, } - }) + ) cli() diff --git a/src/cryptoadvance/specter/config.py b/src/cryptoadvance/specter/config.py index 95f4d3a438..37bb3f9a07 100644 --- a/src/cryptoadvance/specter/config.py +++ b/src/cryptoadvance/specter/config.py @@ -1,4 +1,4 @@ -''' A config module contains static configuration ''' +""" A config module contains static configuration """ import datetime import os import configparser @@ -12,33 +12,38 @@ # BASEDIR = os.path.abspath(os.path.dirname(__file__)) # Loading env-vars from .flaskenv (4 levels above this file) -env_path = Path('../../../..') / '.flaskenv' +env_path = Path("../../../..") / ".flaskenv" load_dotenv(env_path) + def _get_bool_env_var(varname, default=None): value = os.environ.get(varname, default) if value is None: return False - elif isinstance(value, str) and value.lower() == 'false': + elif isinstance(value, str) and value.lower() == "false": return False elif bool(value) is False: return False else: return bool(value) + class BaseConfig(object): - PORT=os.getenv("PORT",25441) - CONNECT_TOR=_get_bool_env_var(os.getenv("CONNECT_TOR","False")) + PORT = os.getenv("PORT", 25441) + CONNECT_TOR = _get_bool_env_var(os.getenv("CONNECT_TOR", "False")) pass + class DevelopmentConfig(BaseConfig): # https://stackoverflow.com/questions/22463939/demystify-flask-app-secret-key - SECRET_KEY='development key' + SECRET_KEY = "development key" + class TestConfig(BaseConfig): - SECRET_KEY='test key' + SECRET_KEY = "test key" + class ProductionConfig(BaseConfig): pass diff --git a/src/cryptoadvance/specter/controller.py b/src/cryptoadvance/specter/controller.py index 9e382efb07..648b9bc6af 100644 --- a/src/cryptoadvance/specter/controller.py +++ b/src/cryptoadvance/specter/controller.py @@ -6,20 +6,39 @@ from mnemonic import Mnemonic from threading import Thread from .key import Key -from.device_manager import get_device_class +from .device_manager import get_device_class from functools import wraps from flask import g, request, redirect, url_for -from flask import Flask, Blueprint, render_template, request, redirect, url_for, jsonify, flash, send_file +from flask import ( + Flask, + Blueprint, + render_template, + request, + redirect, + url_for, + jsonify, + flash, + send_file, +) from flask_login import login_required, login_user, logout_user, current_user from flask_login.config import EXEMPT_METHODS -from .helpers import (alias, get_devices_with_keys_by_type, - get_loglevel, get_version_info, run_shell, set_loglevel, - bcur2base64, get_txid, generate_mnemonic, - get_startblock_by_chain, fslock) +from .helpers import ( + alias, + get_devices_with_keys_by_type, + get_loglevel, + get_version_info, + run_shell, + set_loglevel, + bcur2base64, + get_txid, + generate_mnemonic, + get_startblock_by_chain, + fslock, +) from .specter import Specter from .specter_error import SpecterError from .wallet_manager import purposes @@ -35,62 +54,82 @@ from stem.control import Controller from pathlib import Path -env_path = Path('.') / '.flaskenv' + +env_path = Path(".") / ".flaskenv" from dotenv import load_dotenv + load_dotenv(env_path) from flask import current_app as app -rand = random.randint(0, 1e32) # to force style refresh + +rand = random.randint(0, 1e32) # to force style refresh ########## exception handler ############## @app.errorhandler(Exception) def server_error(e): app.logger.error("Uncaught exception: %s" % e) trace = traceback.format_exc() - return render_template('500.jinja', error=e, traceback=trace), 500 + return render_template("500.jinja", error=e, traceback=trace), 500 + ########## on every request ############### @app.before_request def selfcheck(): """check status before every request""" if app.specter.rpc is not None: - type(app.specter.rpc).counter=0 - if app.config.get('LOGIN_DISABLED'): - app.login('admin') + type(app.specter.rpc).counter = 0 + if app.config.get("LOGIN_DISABLED"): + app.login("admin") ########## template injections ############# @app.context_processor def inject_debug(): - ''' Can be used in all jinja2 templates ''' - return dict(debug=app.config['DEBUG']) + """Can be used in all jinja2 templates""" + return dict(debug=app.config["DEBUG"]) @app.context_processor def inject_tor(): - if app.config['DEBUG']: - return dict(tor_service_id='', tor_enabled=False) - if request.args.get('action', '') == 'stoptor' or request.args.get('action', '') == 'starttor': - if hasattr(current_user, 'is_admin') and current_user.is_admin: + if app.config["DEBUG"]: + return dict(tor_service_id="", tor_enabled=False) + if ( + request.args.get("action", "") == "stoptor" + or request.args.get("action", "") == "starttor" + ): + if hasattr(current_user, "is_admin") and current_user.is_admin: try: - current_hidden_services = app.controller.list_ephemeral_hidden_services() + current_hidden_services = ( + app.controller.list_ephemeral_hidden_services() + ) except Exception: current_hidden_services = [] - if request.args.get('action', '') == 'stoptor' and len(current_hidden_services) != 0: + if ( + request.args.get("action", "") == "stoptor" + and len(current_hidden_services) != 0 + ): stop_hidden_services(app) - if request.args.get('action', '') == 'starttor' and len(current_hidden_services) == 0: + if ( + request.args.get("action", "") == "starttor" + and len(current_hidden_services) == 0 + ): try: start_hidden_service(app) except Exception as e: - flash('Failed to start Tor hidden service.\ + flash( + "Failed to start Tor hidden service.\ Make sure you have Tor running with ControlPort configured and try again.\ -Error returned: {}'.format(e), 'error') - return dict(tor_service_id='', tor_enabled=False) +Error returned: {}".format( + e + ), + "error", + ) + return dict(tor_service_id="", tor_enabled=False) return dict(tor_service_id=app.tor_service_id, tor_enabled=app.tor_enabled) ################ routes #################### -@app.route('/wallets//combine/', methods=['GET', 'POST']) +@app.route("/wallets//combine/", methods=["GET", "POST"]) @login_required def combine(wallet_alias): try: @@ -98,13 +137,10 @@ def combine(wallet_alias): except SpecterError as se: app.logger.error("SpecterError while combine: %s" % se) return render_template("base.jinja", error=se, specter=app.specter, rand=rand) - if request.method == 'POST': + if request.method == "POST": # FIXME: ugly... - txid = request.form.get('txid') - psbts = [ - request.form.get('psbt0').strip(), - request.form.get('psbt1').strip() - ] + txid = request.form.get("txid") + psbts = [request.form.get("psbt0").strip(), request.form.get("psbt1").strip()] raw = {} combined = None @@ -127,7 +163,7 @@ def combine(wallet_alias): # if not - maybe finalized hex tx if not psbt.startswith("cHNi"): raw["hex"] = psbt - combined = psbts[1-i] + combined = psbts[1 - i] # try converting to bytes if "hex" in raw: @@ -152,12 +188,17 @@ def combine(wallet_alias): devices = [] # we get names, but need aliases if "devices_signed" in psbt: - devices = [dev.alias for dev in wallet.devices if dev.name in psbt["devices_signed"]] + devices = [ + dev.alias + for dev in wallet.devices + if dev.name in psbt["devices_signed"] + ] raw["devices"] = devices return json.dumps(raw) - return 'meh' + return "meh" + -@app.route('/wallets//broadcast/', methods=['GET', 'POST']) +@app.route("/wallets//broadcast/", methods=["GET", "POST"]) @login_required def broadcast(wallet_alias): try: @@ -165,28 +206,39 @@ def broadcast(wallet_alias): except SpecterError as se: app.logger.error("SpecterError while broadcast: %s" % se) return render_template("base.jinja", error=se, specter=app.specter, rand=rand) - if request.method == 'POST': - tx = request.form.get('tx') + if request.method == "POST": + tx = request.form.get("tx") res = wallet.rpc.testmempoolaccept([tx])[0] - if res['allowed']: + if res["allowed"]: app.specter.broadcast(tx) wallet.delete_pending_psbt(get_txid(tx)) return jsonify(success=True) else: - return jsonify(success=False, error="Failed to broadcast transaction: transaction is invalid\n%s" % res["reject-reason"]) + return jsonify( + success=False, + error="Failed to broadcast transaction: transaction is invalid\n%s" + % res["reject-reason"], + ) return jsonify(success=False, error="broadcast tx request must use POST") -@app.route('/') + +@app.route("/") @login_required def index(): notify_upgrade() app.specter.check() if len(app.specter.wallet_manager.wallets) > 0: - return redirect("/wallets/%s" % app.specter.wallet_manager.wallets[app.specter.wallet_manager.wallets_names[0]].alias) + return redirect( + "/wallets/%s" + % app.specter.wallet_manager.wallets[ + app.specter.wallet_manager.wallets_names[0] + ].alias + ) + + return redirect("/about") - return redirect('/about') -@app.route('/about') +@app.route("/about") @login_required def about(): notify_upgrade() @@ -194,70 +246,90 @@ def about(): return render_template("base.jinja", specter=app.specter, rand=rand) -@app.route('/login', methods=['GET', 'POST']) + +@app.route("/login", methods=["GET", "POST"]) def login(): - ''' login ''' + """login""" app.specter.check() - if request.method == 'POST': - if app.specter.config['auth'] == 'none': - app.login('admin') + if request.method == "POST": + if app.specter.config["auth"] == "none": + app.login("admin") app.logger.info("AUDIT: Successfull Login no credentials") return redirect_login(request) - if app.specter.config['auth'] == 'rpcpasswordaspin': + if app.specter.config["auth"] == "rpcpasswordaspin": # TODO: check the password via RPC-call if app.specter.rpc is None: - flash("We could not check your password, maybe Bitcoin Core is not running or not configured?","error") + flash( + "We could not check your password, maybe Bitcoin Core is not running or not configured?", + "error", + ) app.logger.info("AUDIT: Failed to check password") - return render_template('login.jinja', specter=app.specter, data={'controller':'controller.login'}), 401 + return ( + render_template( + "login.jinja", + specter=app.specter, + data={"controller": "controller.login"}, + ), + 401, + ) rpc = app.specter.rpc.clone() - rpc.passwd = request.form['password'] + rpc.passwd = request.form["password"] if rpc.test_connection(): - app.login('admin') + app.login("admin") app.logger.info("AUDIT: Successfull Login via RPC-credentials") return redirect_login(request) - elif app.specter.config['auth'] == 'usernamepassword': + elif app.specter.config["auth"] == "usernamepassword": # TODO: This way both "User" and "user" will pass as usernames, should there be strict check on that here? Or should we keep it like this? - username = request.form['username'] - password = request.form['password'] + username = request.form["username"] + password = request.form["password"] user = User.get_user_by_name(app.specter, username) if user: if verify_password(user.password, password): app.login(user.id) return redirect_login(request) # Either invalid method or incorrect credentials - flash('Invalid username or password', "error") + flash("Invalid username or password", "error") app.logger.info("AUDIT: Invalid password login attempt") - return render_template('login.jinja', specter=app.specter, data={'controller':'controller.login'}), 401 + return ( + render_template( + "login.jinja", + specter=app.specter, + data={"controller": "controller.login"}, + ), + 401, + ) else: - if app.config.get('LOGIN_DISABLED'): - app.login('admin') - return redirect('/') - return render_template('login.jinja', specter=app.specter, data={'next':request.args.get('next')}) + if app.config.get("LOGIN_DISABLED"): + app.login("admin") + return redirect("/") + return render_template( + "login.jinja", specter=app.specter, data={"next": request.args.get("next")} + ) + def redirect_login(request): - flash('Logged in successfully.',"info") - if request.form.get('next') and request.form.get('next') != 'None': - response = redirect(request.form['next']) + flash("Logged in successfully.", "info") + if request.form.get("next") and request.form.get("next") != "None": + response = redirect(request.form["next"]) else: - response = redirect(url_for('index')) + response = redirect(url_for("index")) return response -@app.route('/register', methods=['GET', 'POST']) + +@app.route("/register", methods=["GET", "POST"]) def register(): - ''' register ''' + """register""" app.specter.check() - if request.method == 'POST': - username = request.form['username'] - password = hash_password(request.form['password']) - otp = request.form['otp'] + if request.method == "POST": + username = request.form["username"] + password = hash_password(request.form["password"]) + otp = request.form["otp"] user_id = alias(username) - if User.get_user(app.specter, user_id) \ - or User.get_user_by_name(app.specter, username): - flash( - 'Username is already taken, please choose another one', - 'error' - ) - return redirect('/register?otp={}'.format(otp)) + if User.get_user(app.specter, user_id) or User.get_user_by_name( + app.specter, username + ): + flash("Username is already taken, please choose another one", "error") + return redirect("/register?otp={}".format(otp)) if app.specter.burn_new_user_otp(otp): config = { "explorers": { @@ -271,29 +343,29 @@ def register(): user = User(user_id, username, password, config) user.save_info(app.specter) flash( - 'You have registered successfully, \ -please login with your new account to start using Specter' + "You have registered successfully, \ +please login with your new account to start using Specter" ) - return redirect('/login') + return redirect("/login") else: flash( - 'Invalid registration link, \ -please request a new link from the node operator.', - 'error' + "Invalid registration link, \ +please request a new link from the node operator.", + "error", ) - return redirect('/register?otp={}'.format(otp)) - return render_template('register.jinja', specter=app.specter) + return redirect("/register?otp={}".format(otp)) + return render_template("register.jinja", specter=app.specter) -@app.route('/logout', methods=['GET', 'POST']) +@app.route("/logout", methods=["GET", "POST"]) def logout(): logout_user() - flash('You were logged out', "info") + flash("You were logged out", "info") app.specter.clear_user_session() return redirect("/login") -@app.route('/settings/', methods=['GET']) +@app.route("/settings/", methods=["GET"]) @login_required def settings(): if current_user.is_admin: @@ -302,24 +374,24 @@ def settings(): return redirect("/settings/general") -@app.route('/settings/hwi', methods=['GET', 'POST']) +@app.route("/settings/hwi", methods=["GET", "POST"]) @login_required def hwi_settings(): current_version = notify_upgrade() app.specter.check() - if request.method == 'POST': - hwi_bridge_url = request.form['hwi_bridge_url'] + if request.method == "POST": + hwi_bridge_url = request.form["hwi_bridge_url"] app.specter.update_hwi_bridge_url(hwi_bridge_url, current_user) flash("HWIBridge URL is updated! Don't forget to whitelist Specter!") return render_template( "settings/hwi_settings.jinja", specter=app.specter, current_version=current_version, - rand=rand + rand=rand, ) -@app.route('/settings/general', methods=['GET', 'POST']) +@app.route("/settings/general", methods=["GET", "POST"]) @login_required def general_settings(): current_version = notify_upgrade() @@ -327,14 +399,14 @@ def general_settings(): explorer = app.specter.explorer loglevel = get_loglevel(app) unit = app.specter.unit - if request.method == 'POST': - action = request.form['action'] - explorer = request.form['explorer'] - unit = request.form['unit'] - validate_merkleproof_bool = request.form.get('validatemerkleproof') == "on" + if request.method == "POST": + action = request.form["action"] + explorer = request.form["explorer"] + unit = request.form["unit"] + validate_merkleproof_bool = request.form.get("validatemerkleproof") == "on" if current_user.is_admin: - loglevel = request.form['loglevel'] + loglevel = request.form["loglevel"] if action == "save": if current_user.is_admin: @@ -342,25 +414,27 @@ def general_settings(): app.specter.update_explorer(explorer, current_user) app.specter.update_unit(unit, current_user) - app.specter.update_merkleproof_settings(validate_bool=validate_merkleproof_bool) + app.specter.update_merkleproof_settings( + validate_bool=validate_merkleproof_bool + ) app.specter.check() elif action == "backup": return send_file( app.specter.specter_backup_file(), - attachment_filename='specter-backup.zip', - as_attachment=True + attachment_filename="specter-backup.zip", + as_attachment=True, ) elif action == "restore": - restore_devices = json.loads(request.form['restoredevices']) - restore_wallets = json.loads(request.form['restorewallets']) + restore_devices = json.loads(request.form["restoredevices"]) + restore_wallets = json.loads(request.form["restorewallets"]) for device in restore_devices: with fslock: with open( os.path.join( app.specter.device_manager.data_folder, - "%s.json" % device['alias'] + "%s.json" % device["alias"], ), - "w" + "w", ) as file: file.write(json.dumps(device, indent=4)) app.specter.device_manager.update() @@ -370,41 +444,41 @@ def general_settings(): try: app.specter.wallet_manager.rpc.createwallet( os.path.join( - app.specter.wallet_manager.rpc_path, - wallet['alias'] + app.specter.wallet_manager.rpc_path, wallet["alias"] ), - True + True, ) except Exception as e: # if wallet already exists in Bitcoin Core # continue with the existing one - if 'already exists' not in str(e): + if "already exists" not in str(e): flash( - 'Failed to import wallet {}, error: {}' - .format(wallet['name'], e), - 'error' + "Failed to import wallet {}, error: {}".format( + wallet["name"], e + ), + "error", ) continue with fslock: with open( os.path.join( app.specter.wallet_manager.working_folder, - "%s.json" % wallet['alias'] + "%s.json" % wallet["alias"], ), - "w" + "w", ) as file: file.write(json.dumps(wallet, indent=4)) app.specter.wallet_manager.update() try: wallet_obj = app.specter.wallet_manager.get_by_alias( - wallet['alias'] + wallet["alias"] ) try: wallet_obj.rpc.rescanblockchain( - wallet['blockheight'] - if 'blockheight' in wallet + wallet["blockheight"] + if "blockheight" in wallet else get_startblock_by_chain(app.specter), - timeout=1 + timeout=1, ) app.logger.info("Rescanning Blockchain ...") rescanning = True @@ -416,64 +490,62 @@ def general_settings(): "Exception while rescanning blockchain: {}".format(e) ) flash( - "Failed to perform rescan for wallet: {}".format(e), - 'error' + "Failed to perform rescan for wallet: {}".format(e), "error" ) wallet_obj.getdata() except Exception: - flash( - 'Failed to import wallet {}' - .format(wallet['name']), - 'error' - ) - flash('Specter data was successfully loaded from backup.', 'info') + flash("Failed to import wallet {}".format(wallet["name"]), "error") + flash("Specter data was successfully loaded from backup.", "info") if rescanning: - flash('Wallets are rescanning for transactions history.\n\ -This may take a few hours to complete.', 'info') + flash( + "Wallets are rescanning for transactions history.\n\ +This may take a few hours to complete.", + "info", + ) return render_template( "settings/general_settings.jinja", explorer=explorer, loglevel=loglevel, - validate_merkle_proofs=app.specter.config.get('validate_merkle_proofs') is True, + validate_merkle_proofs=app.specter.config.get("validate_merkle_proofs") is True, unit=unit, specter=app.specter, current_version=current_version, - rand=rand + rand=rand, ) -@app.route('/settings/bitcoin_core', methods=['GET', 'POST']) +@app.route("/settings/bitcoin_core", methods=["GET", "POST"]) @login_required def bitcoin_core_settings(): current_version = notify_upgrade() app.specter.check() if not current_user.is_admin: - flash('Only an admin is allowed to access this page.', 'error') + flash("Only an admin is allowed to access this page.", "error") return redirect("/") - rpc = app.specter.config['rpc'] - user = rpc['user'] - passwd = rpc['password'] - port = rpc['port'] - host = rpc['host'] - protocol = 'http' - autodetect = rpc['autodetect'] - datadir = rpc['datadir'] + rpc = app.specter.config["rpc"] + user = rpc["user"] + passwd = rpc["password"] + port = rpc["port"] + host = rpc["host"] + protocol = "http" + autodetect = rpc["autodetect"] + datadir = rpc["datadir"] err = None if "protocol" in rpc: protocol = rpc["protocol"] test = None - if request.method == 'POST': - action = request.form['action'] + if request.method == "POST": + action = request.form["action"] if current_user.is_admin: - autodetect = 'autodetect' in request.form + autodetect = "autodetect" in request.form if autodetect: - datadir = request.form['datadir'] - user = request.form['username'] - passwd = request.form['password'] - port = request.form['port'] - host = request.form['host'] + datadir = request.form["datadir"] + user = request.form["username"] + passwd = request.form["password"] + port = request.form["port"] + host = request.form["host"] # protocol://host if "://" in host: @@ -490,10 +562,10 @@ def bitcoin_core_settings(): host=host, protocol=protocol, autodetect=autodetect, - datadir=datadir + datadir=datadir, ) except Exception as e: - err = 'Fail to connect to the node configured: {}'.format(e) + err = "Fail to connect to the node configured: {}".format(e) elif action == "save": if current_user.is_admin: app.specter.update_rpc( @@ -503,7 +575,7 @@ def bitcoin_core_settings(): host=host, protocol=protocol, autodetect=autodetect, - datadir=datadir + datadir=datadir, ) app.specter.check() @@ -520,42 +592,38 @@ def bitcoin_core_settings(): specter=app.specter, current_version=current_version, error=err, - rand=rand + rand=rand, ) -@app.route('/settings/auth', methods=['GET', 'POST']) +@app.route("/settings/auth", methods=["GET", "POST"]) @login_required def auth_settings(): current_version = notify_upgrade() app.specter.check() - auth = app.specter.config['auth'] + auth = app.specter.config["auth"] new_otp = -1 users = None if current_user.is_admin and auth == "usernamepassword": - users = [ - user - for user in User.get_all_users(app.specter) - if not user.is_admin - ] - if request.method == 'POST': - action = request.form['action'] + users = [user for user in User.get_all_users(app.specter) if not user.is_admin] + if request.method == "POST": + action = request.form["action"] if action == "save": - if 'specter_username' in request.form: - specter_username = request.form['specter_username'] - specter_password = request.form['specter_password'] + if "specter_username" in request.form: + specter_username = request.form["specter_username"] + specter_password = request.form["specter_password"] else: specter_username = None specter_password = None if current_user.is_admin: - auth = request.form['auth'] + auth = request.form["auth"] if specter_username: if current_user.username != specter_username: if User.get_user_by_name(app.specter, specter_username): flash( - 'Username is already taken, please choose another one', - "error" + "Username is already taken, please choose another one", + "error", ) return render_template( "settings/auth_settings.jinja", @@ -564,7 +632,7 @@ def auth_settings(): users=users, specter=app.specter, current_version=current_version, - rand=rand + rand=rand, ) current_user.username = specter_username if specter_password: @@ -581,31 +649,50 @@ def auth_settings(): ] else: users = None - app.config['LOGIN_DISABLED'] = False + app.config["LOGIN_DISABLED"] = False else: users = None - app.config['LOGIN_DISABLED'] = True + app.config["LOGIN_DISABLED"] = True app.specter.check() elif action == "adduser": if current_user.is_admin: new_otp = random.randint(100000, 999999) - app.specter.add_new_user_otp({ 'otp': new_otp, 'created_at': time.time() }) - flash('New user link generated successfully: {}register?otp={}'.format(request.url_root, new_otp), 'info') + app.specter.add_new_user_otp( + {"otp": new_otp, "created_at": time.time()} + ) + flash( + "New user link generated successfully: {}register?otp={}".format( + request.url_root, new_otp + ), + "info", + ) else: - flash('Error: Only the admin account can issue new registration links.', 'error') + flash( + "Error: Only the admin account can issue new registration links.", + "error", + ) elif action == "deleteuser": - delete_user = request.form['deleteuser'] + delete_user = request.form["deleteuser"] if current_user.is_admin: user = User.get_user(app.specter, delete_user) if user: user.delete(app.specter) - users = [user for user in User.get_all_users(app.specter) if not user.is_admin] - flash('User {} was deleted successfully'.format(user.username), 'info') + users = [ + user + for user in User.get_all_users(app.specter) + if not user.is_admin + ] + flash( + "User {} was deleted successfully".format(user.username), "info" + ) else: - flash('Error: failed to delete user, invalid user ID was given', 'error') + flash( + "Error: failed to delete user, invalid user ID was given", + "error", + ) else: - flash('Error: Only the admin account can delete users', 'error') + flash("Error: Only the admin account can delete users", "error") return render_template( "settings/auth_settings.jinja", auth=auth, @@ -613,13 +700,14 @@ def auth_settings(): users=users, specter=app.specter, current_version=current_version, - rand=rand + rand=rand, ) ################# wallet management ##################### -@app.route('/new_wallet/') + +@app.route("/new_wallet/") @login_required def new_wallet_type(): app.specter.check() @@ -627,13 +715,15 @@ def new_wallet_type(): if app.specter.chain is None: err = "Configure Bitcoin Core to create wallets" return render_template("base.jinja", error=err, specter=app.specter, rand=rand) - return render_template("wallet/new_wallet/new_wallet_type.jinja", specter=app.specter, rand=rand) + return render_template( + "wallet/new_wallet/new_wallet_type.jinja", specter=app.specter, rand=rand + ) -@app.route('/new_wallet//', methods=['GET', 'POST']) +@app.route("/new_wallet//", methods=["GET", "POST"]) @login_required def new_wallet(wallet_type): - wallet_types = ['simple', 'multisig', 'import_wallet'] + wallet_types = ["simple", "multisig", "import_wallet"] if wallet_type not in wallet_types: err = "Unknown wallet type requested" return render_template("base.jinja", specter=app.specter, rand=rand) @@ -651,23 +741,29 @@ def new_wallet(wallet_type): if sigs_total < 2: err = "You need more devices to do multisig" return render_template("base.jinja", specter=app.specter, rand=rand) - sigs_required = sigs_total*2//3 + sigs_required = sigs_total * 2 // 3 if sigs_required < 2: sigs_required = 2 else: sigs_total = 1 sigs_required = 1 - if request.method == 'POST': - action = request.form['action'] + if request.method == "POST": + action = request.form["action"] if action == "importwallet": - wallet_data = json.loads(request.form['wallet_data'].replace("'", "h")) - wallet_name = wallet_data['label'] if 'label' in wallet_data else 'Imported Wallet' - startblock = wallet_data['blockheight'] if 'blockheight' in wallet_data else app.specter.wallet_manager.rpc.getblockcount() + wallet_data = json.loads(request.form["wallet_data"].replace("'", "h")) + wallet_name = ( + wallet_data["label"] if "label" in wallet_data else "Imported Wallet" + ) + startblock = ( + wallet_data["blockheight"] + if "blockheight" in wallet_data + else app.specter.wallet_manager.rpc.getblockcount() + ) try: descriptor = Descriptor.parse( - AddChecksum(wallet_data['descriptor'].split('#')[0]), - testnet=app.specter.chain != 'main' + AddChecksum(wallet_data["descriptor"].split("#")[0]), + testnet=app.specter.chain != "main", ) if descriptor is None: err = "Invalid wallet descriptor." @@ -681,17 +777,17 @@ def new_wallet(wallet_type): sigs_total = descriptor.multisig_N sigs_required = descriptor.multisig_M if descriptor.wpkh: - address_type = 'wpkh' + address_type = "wpkh" elif descriptor.wsh: - address_type = 'wsh' + address_type = "wsh" elif descriptor.sh_wpkh: - address_type = 'sh-wpkh' + address_type = "sh-wpkh" elif descriptor.sh_wsh: - address_type = 'sh-wsh' + address_type = "sh-wsh" elif descriptor.sh: - address_type = 'sh-wsh' + address_type = "sh-wsh" else: - address_type = 'pkh' + address_type = "pkh" keys = [] cosigners = [] unknown_cosigners = [] @@ -706,17 +802,21 @@ def new_wallet(wallet_type): for device in app.specter.device_manager.devices: cosigner = app.specter.device_manager.devices[device] if descriptor.origin_fingerprint[i] is None: - descriptor.origin_fingerprint[i] = '' + descriptor.origin_fingerprint[i] = "" if descriptor.origin_path[i] is None: - descriptor.origin_path[i] = \ - descriptor.origin_fingerprint[i] + descriptor.origin_path[ + i + ] = descriptor.origin_fingerprint[i] for key in cosigner.keys: - if key.fingerprint + \ - key.derivation.replace('m', '') == \ - descriptor.origin_fingerprint[i] + \ - descriptor.origin_path[i].replace( - "'", 'h' - ): + if key.fingerprint + key.derivation.replace( + "m", "" + ) == descriptor.origin_fingerprint[ + i + ] + descriptor.origin_path[ + i + ].replace( + "'", "h" + ): keys.append(key) cosigners.append(cosigner) cosigner_found = True @@ -724,23 +824,33 @@ def new_wallet(wallet_type): if cosigner_found: break if not cosigner_found: - desc_key = Key.parse_xpub('[{}{}]{}'.format( - descriptor.origin_fingerprint[i], - descriptor.origin_path[i], - descriptor.base_key[i], - )) + desc_key = Key.parse_xpub( + "[{}{}]{}".format( + descriptor.origin_fingerprint[i], + descriptor.origin_path[i], + descriptor.base_key[i], + ) + ) unknown_cosigners.append(desc_key) # raise Exception('Could not find device with matching key to import wallet') - wallet_type = 'multisig' if sigs_total > 1 else 'simple' - createwallet = 'createwallet' in request.form + wallet_type = "multisig" if sigs_total > 1 else "simple" + createwallet = "createwallet" in request.form if createwallet: - wallet_name = request.form['wallet_name'] + wallet_name = request.form["wallet_name"] for i, unknown_cosigner in enumerate(unknown_cosigners): - unknown_cosigner_name = request.form['unknown_cosigner_{}_name'.format(i)] - device = app.specter.device_manager.add_device(name=unknown_cosigner_name, device_type='other', keys=[unknown_cosigner]) + unknown_cosigner_name = request.form[ + "unknown_cosigner_{}_name".format(i) + ] + device = app.specter.device_manager.add_device( + name=unknown_cosigner_name, + device_type="other", + keys=[unknown_cosigner], + ) keys.append(unknown_cosigner) cosigners.append(device) - wallet = app.specter.wallet_manager.create_wallet(wallet_name, sigs_required, address_type, keys, cosigners) + wallet = app.specter.wallet_manager.create_wallet( + wallet_name, sigs_required, address_type, keys, cosigners + ) flash("Wallet imported successfully", "info") try: wallet.rpc.rescanblockchain(startblock, timeout=1) @@ -749,8 +859,12 @@ def new_wallet(wallet_type): # this is normal behavior in our usecase pass except Exception as e: - app.logger.error("Exception while rescanning blockchain: %e" % e) - flash("Failed to perform rescan for wallet: %r" % e, 'error') + app.logger.error( + "Exception while rescanning blockchain: %e" % e + ) + flash( + "Failed to perform rescan for wallet: %r" % e, "error" + ) wallet.getdata() return redirect("/wallets/%s/" % wallet.alias) else: @@ -765,25 +879,37 @@ def new_wallet(wallet_type): sigs_total=sigs_total, error=err, specter=app.specter, - rand=rand + rand=rand, ) except Exception as e: err = "%r" % e if err: - return render_template("wallet/new_wallet/new_wallet_type.jinja", error="Failed to import wallet: " + err, specter=app.specter, rand=rand) + return render_template( + "wallet/new_wallet/new_wallet_type.jinja", + error="Failed to import wallet: " + err, + specter=app.specter, + rand=rand, + ) else: - wallet_name = request.form['wallet_name'] + wallet_name = request.form["wallet_name"] if wallet_name in app.specter.wallet_manager.wallets_names: err = "Wallet already exists" - address_type = request.form['type'] - sigs_total = int(request.form.get('sigs_total', 1)) - sigs_required = int(request.form.get('sigs_required', 1)) - - if action == 'device' and err is None: - cosigners = [app.specter.device_manager.get_by_alias(alias) for alias in request.form.getlist('devices')] + address_type = request.form["type"] + sigs_total = int(request.form.get("sigs_total", 1)) + sigs_required = int(request.form.get("sigs_required", 1)) + + if action == "device" and err is None: + cosigners = [ + app.specter.device_manager.get_by_alias(alias) + for alias in request.form.getlist("devices") + ] if len(cosigners) != sigs_total: - err = "Select the device" if sigs_total == 1 else "Select all the cosigners" + err = ( + "Select the device" + if sigs_total == 1 + else "Select all the cosigners" + ) return render_template( "wallet/new_wallet/new_wallet.jinja", wallet_type=wallet_type, @@ -792,33 +918,36 @@ def new_wallet(wallet_type): sigs_total=sigs_total, error=err, specter=app.specter, - rand=rand + rand=rand, ) devices = get_devices_with_keys_by_type(app, cosigners, address_type) for device in devices: if len(device.keys) == 0: - err = "Device %s doesn't have keys matching this wallet type" % device.name - break + err = ( + "Device %s doesn't have keys matching this wallet type" + % device.name + ) + break return render_template( "wallet/new_wallet/new_wallet_keys.jinja", - purposes=purposes, + purposes=purposes, wallet_type=address_type, - wallet_name=wallet_name, + wallet_name=wallet_name, cosigners=devices, - sigs_required=sigs_required, - sigs_total=sigs_total, + sigs_required=sigs_required, + sigs_total=sigs_total, error=err, specter=app.specter, - rand=rand + rand=rand, ) - if action == 'key' and err is None: + if action == "key" and err is None: keys = [] cosigners = [] devices = [] for i in range(sigs_total): try: - key = request.form['key%d' % i] - cosigner_name = request.form['cosigner%d' % i] + key = request.form["key%d" % i] + cosigner_name = request.form["cosigner%d" % i] cosigner = app.specter.device_manager.get_by_alias(cosigner_name) cosigners.append(cosigner) for k in cosigner.keys: @@ -832,25 +961,27 @@ def new_wallet(wallet_type): err = "Did you select enough keys?" return render_template( "wallet/new_wallet/new_wallet_keys.jinja", - purposes=purposes, + purposes=purposes, wallet_type=address_type, - wallet_name=wallet_name, + wallet_name=wallet_name, cosigners=devices, - sigs_required=sigs_required, - sigs_total=sigs_total, + sigs_required=sigs_required, + sigs_total=sigs_total, error=err, specter=app.specter, - rand=rand + rand=rand, ) # create a wallet here - wallet = app.specter.wallet_manager.create_wallet(wallet_name, sigs_required, address_type, keys, cosigners) + wallet = app.specter.wallet_manager.create_wallet( + wallet_name, sigs_required, address_type, keys, cosigners + ) app.logger.info("Created Wallet %s" % wallet_name) - rescan_blockchain = 'rescanblockchain' in request.form + rescan_blockchain = "rescanblockchain" in request.form if rescan_blockchain: # old wallet - import more addresses wallet.keypoolrefill(0, wallet.IMPORT_KEYPOOL, change=False) wallet.keypoolrefill(0, wallet.IMPORT_KEYPOOL, change=True) - if 'utxo' in request.form.get('full_rescan_option'): + if "utxo" in request.form.get("full_rescan_option"): explorer = None if "use_explorer" in request.form: explorer = app.specter.get_default_explorer() @@ -859,14 +990,16 @@ def new_wallet(wallet_type): app.specter.utxorescanwallet = wallet.alias else: app.logger.info("Rescanning Blockchain ...") - startblock = int(request.form['startblock']) + startblock = int(request.form["startblock"]) try: wallet.rpc.rescanblockchain(startblock, timeout=1) except requests.exceptions.ReadTimeout: # this is normal behavior in our usecase pass except Exception as e: - app.logger.error("Exception while rescanning blockchain: %e" % e) + app.logger.error( + "Exception while rescanning blockchain: %e" % e + ) err = "%r" % e wallet.getdata() return redirect("/wallets/%s/" % wallet.alias) @@ -879,11 +1012,11 @@ def new_wallet(wallet_type): sigs_total=sigs_total, error=err, specter=app.specter, - rand=rand + rand=rand, ) -@app.route('/wallets//') +@app.route("/wallets//") @login_required def wallet(wallet_alias): app.specter.check() @@ -898,85 +1031,77 @@ def wallet(wallet_alias): return redirect("/wallets/%s/tx/" % wallet_alias) -@app.route('/wallets_overview/') +@app.route("/wallets_overview/") @login_required def wallets_overview(): app.specter.check() - idx = int(request.args.get('idx', default=0)) + idx = int(request.args.get("idx", default=0)) return render_template( "wallet/wallets_overview.jinja", idx=idx, history=True, specter=app.specter, - rand=rand + rand=rand, ) -@app.route('/singlesig_setup_wizard/', methods=['GET', 'POST']) +@app.route("/singlesig_setup_wizard/", methods=["GET", "POST"]) @login_required def singlesig_setup_wizard(): app.specter.check() err = None if request.method == "POST": - xpubs = request.form['xpubs'] + xpubs = request.form["xpubs"] if not xpubs: err = "xpubs name must not be empty" keys, failed = Key.parse_xpubs(xpubs) if len(failed) > 0: err = "Failed to parse these xpubs:\n" + "\n".join(failed) - device_type = request.form.get('devices') + device_type = request.form.get("devices") device_name = get_device_class(device_type).name i = 2 while device_name in [ device.name for device in app.specter.device_manager.devices.values() ]: - device_name = "%s %d" % ( - get_device_class(device_type).name, - i - ) + device_name = "%s %d" % (get_device_class(device_type).name, i) i += 1 if err is None: device = app.specter.device_manager.add_device( - name=device_name, - device_type=device_type, - keys=keys + name=device_name, device_type=device_type, keys=keys ) - wallet_name = request.form['wallet_name'] + wallet_name = request.form["wallet_name"] if wallet_name in app.specter.wallet_manager.wallets_names: err = "Wallet already exists" - address_type = request.form['type'] + address_type = request.form["type"] wallet_key = [ - key for key in device.keys if key.key_type == address_type and ( - key.xpub.startswith("xpub") != (app.specter.chain != 'main') - ) + key + for key in device.keys + if key.key_type == address_type + and (key.xpub.startswith("xpub") != (app.specter.chain != "main")) ] if len(wallet_key) != 1: - err = 'Device key was not imported properly. Please make\ - sure your device is on the right network and try again.' + err = "Device key was not imported properly. Please make\ + sure your device is on the right network and try again." if err: app.specter.device_manager.remove_device( device, app.specter.wallet_manager, bitcoin_datadir=app.specter.bitcoin_datadir, - chain=app.specter.chain + chain=app.specter.chain, ) return render_template( "wizards/singlesig_setup_wizard.jinja", error=err, specter=app.specter, - rand=rand + rand=rand, ) wallet = app.specter.wallet_manager.create_wallet( - wallet_name, - 1, - address_type, - wallet_key, - [device] + wallet_name, 1, address_type, wallet_key, [device] ) app.logger.info("Created Wallet %s" % wallet_name) - rescan_blockchain = request.form['rescan'] == 'true' + rescan_blockchain = request.form["rescan"] == "true" if rescan_blockchain: # old wallet - import more addresses wallet.keypoolrefill(0, wallet.IMPORT_KEYPOOL, change=False) @@ -989,19 +1114,17 @@ def singlesig_setup_wizard(): app.specter.utxorescanwallet = wallet.alias return redirect("/wallets/%s/" % wallet.alias) return render_template( - "wizards/singlesig_setup_wizard.jinja", - specter=app.specter, - rand=rand + "wizards/singlesig_setup_wizard.jinja", specter=app.specter, rand=rand ) -@app.route('/wallets//tx/') +@app.route("/wallets//tx/") @login_required def wallet_tx(wallet_alias): return redirect("/wallets/%s/tx/history" % wallet_alias) -@app.route('/wallets//tx/history/') +@app.route("/wallets//tx/history/") @login_required def wallet_tx_history(wallet_alias): app.specter.check() @@ -1010,11 +1133,20 @@ def wallet_tx_history(wallet_alias): except SpecterError as se: app.logger.error("SpecterError while wallet_tx: %s" % se) return render_template("base.jinja", error=se, specter=app.specter, rand=rand) - idx = int(request.args.get('idx', default=0)) + idx = int(request.args.get("idx", default=0)) + + return render_template( + "wallet/history/txs/wallet_tx.jinja", + idx=idx, + wallet_alias=wallet_alias, + wallet=wallet, + history=True, + specter=app.specter, + rand=rand, + ) - return render_template("wallet/history/txs/wallet_tx.jinja", idx=idx, wallet_alias=wallet_alias, wallet=wallet, history=True, specter=app.specter, rand=rand) -@app.route('/wallets//tx/utxo/', methods=['GET', 'POST']) +@app.route("/wallets//tx/utxo/", methods=["GET", "POST"]) @login_required def wallet_tx_utxo(wallet_alias): app.specter.check() @@ -1023,21 +1155,30 @@ def wallet_tx_utxo(wallet_alias): except SpecterError as se: app.logger.error("SpecterError while wallet_addresses: %s" % se) return render_template("base.jinja", error=se, specter=app.specter, rand=rand) - viewtype = 'address' if request.args.get('view') != 'label' else 'label' + viewtype = "address" if request.args.get("view") != "label" else "label" if request.method == "POST": - action = request.form['action'] + action = request.form["action"] if action == "updatelabel": - label = request.form['label'] - account = request.form['account'] - if viewtype == 'address': + label = request.form["label"] + account = request.form["account"] + if viewtype == "address": wallet.setlabel(account, label) else: for address in wallet.addresses_on_label(account): wallet.setlabel(address, label) wallet.getdata() - return render_template("wallet/history/utxo/wallet_utxo.jinja", wallet_alias=wallet_alias, wallet=wallet, history=False, viewtype=viewtype, specter=app.specter, rand=rand) + return render_template( + "wallet/history/utxo/wallet_utxo.jinja", + wallet_alias=wallet_alias, + wallet=wallet, + history=False, + viewtype=viewtype, + specter=app.specter, + rand=rand, + ) + -@app.route('/wallets//receive/', methods=['GET', 'POST']) +@app.route("/wallets//receive/", methods=["GET", "POST"]) @login_required def wallet_receive(wallet_alias): app.specter.check() @@ -1047,31 +1188,38 @@ def wallet_receive(wallet_alias): app.logger.error("SpecterError while wallet_receive: %s" % se) return render_template("base.jinja", error=se, specter=app.specter, rand=rand) if request.method == "POST": - action = request.form['action'] + action = request.form["action"] if action == "newaddress": wallet.getnewaddress() elif action == "updatelabel": - label = request.form['label'] + label = request.form["label"] wallet.setlabel(wallet.address, label) if wallet.is_current_address_used: wallet.getnewaddress() - return render_template("wallet/receive/wallet_receive.jinja", wallet_alias=wallet_alias, wallet=wallet, specter=app.specter, rand=rand) + return render_template( + "wallet/receive/wallet_receive.jinja", + wallet_alias=wallet_alias, + wallet=wallet, + specter=app.specter, + rand=rand, + ) -@app.route('/get_fee/') + +@app.route("/get_fee/") @login_required def fees(blocks): res = app.specter.estimatesmartfee(int(blocks)) return res -@app.route('/get_txout_set_info') +@app.route("/get_txout_set_info") @login_required def txout_set_info(): res = app.specter.rpc.gettxoutsetinfo() return res -@app.route('/wallets//send') +@app.route("/wallets//send") @login_required def wallet_send(wallet_alias): app.specter.check() @@ -1081,11 +1229,12 @@ def wallet_send(wallet_alias): app.logger.error("SpecterError while wallet_send: %s" % se) return render_template("base.jinja", error=se, specter=app.specter, rand=rand) if len(wallet.pending_psbts) > 0: - return redirect(url_for('wallet_sendpending', wallet_alias=wallet_alias)) + return redirect(url_for("wallet_sendpending", wallet_alias=wallet_alias)) else: - return redirect(url_for('wallet_sendnew', wallet_alias=wallet_alias)) + return redirect(url_for("wallet_sendnew", wallet_alias=wallet_alias)) + -@app.route('/wallets//send/new', methods=['GET', 'POST']) +@app.route("/wallets//send/new", methods=["GET", "POST"]) @login_required def wallet_sendnew(wallet_alias): app.specter.check() @@ -1100,45 +1249,41 @@ def wallet_sendnew(wallet_alias): amounts = [0] fee_rate = 0.0 err = None - ui_option = 'ui' - recipients_txt = '' + ui_option = "ui" + recipients_txt = "" if request.method == "POST": - action = request.form['action'] + action = request.form["action"] if action == "createpsbt": i = 0 addresses = [] labels = [] amounts = [] - ui_option = request.form.get('ui_option') - if 'ui' in ui_option: - while 'address_{}'.format(i) in request.form: - addresses.append(request.form['address_{}'.format(i)]) - amounts.append( - float(request.form['btc_amount_{}'.format(i)]) - ) - labels.append(request.form['label_{}'.format(i)]) - if request.form['label_{}'.format(i)] != '': + ui_option = request.form.get("ui_option") + if "ui" in ui_option: + while "address_{}".format(i) in request.form: + addresses.append(request.form["address_{}".format(i)]) + amounts.append(float(request.form["btc_amount_{}".format(i)])) + labels.append(request.form["label_{}".format(i)]) + if request.form["label_{}".format(i)] != "": wallet.setlabel(addresses[i], labels[i]) i += 1 else: - recipients_txt = request.form['recipients'] + recipients_txt = request.form["recipients"] for output in recipients_txt.splitlines(): - addresses.append(output.split(',')[0].strip()) - if request.form.get('amount_unit_text') == 'sat': - amounts.append( - float(output.split(',')[1].strip()) / 1e8 - ) + addresses.append(output.split(",")[0].strip()) + if request.form.get("amount_unit_text") == "sat": + amounts.append(float(output.split(",")[1].strip()) / 1e8) else: - amounts.append(float(output.split(',')[1].strip())) + amounts.append(float(output.split(",")[1].strip())) subtract = bool(request.form.get("subtract", False)) subtract_from = int(request.form.get("subtract_from", 1)) - 1 - selected_coins = request.form.getlist('coinselect') + selected_coins = request.form.getlist("coinselect") app.logger.info("selected coins: {}".format(selected_coins)) - if 'dynamic' in request.form.get('fee_options'): - fee_rate = float(request.form.get('fee_rate_dynamic')) * 1e5 + if "dynamic" in request.form.get("fee_options"): + fee_rate = float(request.form.get("fee_rate_dynamic")) * 1e5 else: - if request.form.get('fee_rate'): - fee_rate = float(request.form.get('fee_rate')) + if request.form.get("fee_rate"): + fee_rate = float(request.form.get("fee_rate")) try: psbt = wallet.createpsbt( addresses, @@ -1147,7 +1292,7 @@ def wallet_sendnew(wallet_alias): subtract_from=subtract_from, fee_rate=fee_rate, selected_coins=selected_coins, - readonly='estimate_fee' in request.form + readonly="estimate_fee" in request.form, ) if psbt is None: err = "Probably you don't have enough funds, or something else..." @@ -1160,63 +1305,104 @@ def wallet_sendnew(wallet_alias): except Exception as e: err = e if err is None: - if 'estimate_fee' in request.form: + if "estimate_fee" in request.form: return psbt - return render_template("wallet/send/sign/wallet_send_sign_psbt.jinja", psbt=psbt, labels=labels, - wallet_alias=wallet_alias, wallet=wallet, - specter=app.specter, rand=rand) + return render_template( + "wallet/send/sign/wallet_send_sign_psbt.jinja", + psbt=psbt, + labels=labels, + wallet_alias=wallet_alias, + wallet=wallet, + specter=app.specter, + rand=rand, + ) elif action == "importpsbt": try: b64psbt = "".join(request.form["rawpsbt"].split()) psbt = wallet.importpsbt(b64psbt) except Exception as e: flash("Could not import PSBT: %s" % e, "error") - return redirect(url_for('wallet_importpsbt', wallet_alias=wallet_alias)) - return render_template("wallet/send/sign/wallet_send_sign_psbt.jinja", psbt=psbt, labels=labels, - wallet_alias=wallet_alias, wallet=wallet, - specter=app.specter, rand=rand) + return redirect(url_for("wallet_importpsbt", wallet_alias=wallet_alias)) + return render_template( + "wallet/send/sign/wallet_send_sign_psbt.jinja", + psbt=psbt, + labels=labels, + wallet_alias=wallet_alias, + wallet=wallet, + specter=app.specter, + rand=rand, + ) elif action == "openpsbt": psbt = ast.literal_eval(request.form["pending_psbt"]) - return render_template("wallet/send/sign/wallet_send_sign_psbt.jinja", psbt=psbt, labels=labels, - wallet_alias=wallet_alias, wallet=wallet, - specter=app.specter, rand=rand) - elif action == 'deletepsbt': + return render_template( + "wallet/send/sign/wallet_send_sign_psbt.jinja", + psbt=psbt, + labels=labels, + wallet_alias=wallet_alias, + wallet=wallet, + specter=app.specter, + rand=rand, + ) + elif action == "deletepsbt": try: - wallet.delete_pending_psbt(ast.literal_eval(request.form["pending_psbt"])["tx"]["txid"]) + wallet.delete_pending_psbt( + ast.literal_eval(request.form["pending_psbt"])["tx"]["txid"] + ) except Exception as e: flash("Could not delete Pending PSBT!", "error") - elif action == 'signhotwallet': - passphrase = request.form['passphrase'] + elif action == "signhotwallet": + passphrase = request.form["passphrase"] psbt = ast.literal_eval(request.form["psbt"]) - b64psbt = wallet.pending_psbts[psbt['tx']['txid']]['base64'] - device = request.form['device'] - if 'devices_signed' not in psbt or device not in psbt['devices_signed']: + b64psbt = wallet.pending_psbts[psbt["tx"]["txid"]]["base64"] + device = request.form["device"] + if "devices_signed" not in psbt or device not in psbt["devices_signed"]: try: - signed_psbt = app.specter.device_manager.get_by_alias(device).sign_psbt(b64psbt, wallet, passphrase) - if signed_psbt['complete']: - if 'devices_signed' not in psbt: - psbt['devices_signed'] = [] + signed_psbt = app.specter.device_manager.get_by_alias( + device + ).sign_psbt(b64psbt, wallet, passphrase) + if signed_psbt["complete"]: + if "devices_signed" not in psbt: + psbt["devices_signed"] = [] # TODO: This uses device name, but should use device alias... - psbt['devices_signed'].append(app.specter.device_manager.get_by_alias(device).name) - psbt['sigs_count'] = len(psbt['devices_signed']) + psbt["devices_signed"].append( + app.specter.device_manager.get_by_alias(device).name + ) + psbt["sigs_count"] = len(psbt["devices_signed"]) raw = wallet.rpc.finalizepsbt(b64psbt) if "hex" in raw: psbt["raw"] = raw["hex"] - signed_psbt = signed_psbt['psbt'] + signed_psbt = signed_psbt["psbt"] except Exception as e: signed_psbt = None flash("Failed to sign PSBT: %s" % e, "error") else: signed_psbt = None flash("Device already signed the PSBT", "error") - return render_template("wallet/send/sign/wallet_send_sign_psbt.jinja", signed_psbt=signed_psbt, psbt=psbt, labels=labels, - wallet_alias=wallet_alias, wallet=wallet, - specter=app.specter, rand=rand) - return render_template("wallet/send/new/wallet_send.jinja", psbt=psbt, ui_option=ui_option, recipients_txt=recipients_txt, - labels=labels, wallet_alias=wallet_alias, wallet=wallet, - specter=app.specter, rand=rand, error=err) - -@app.route('/wallets//send/import') + return render_template( + "wallet/send/sign/wallet_send_sign_psbt.jinja", + signed_psbt=signed_psbt, + psbt=psbt, + labels=labels, + wallet_alias=wallet_alias, + wallet=wallet, + specter=app.specter, + rand=rand, + ) + return render_template( + "wallet/send/new/wallet_send.jinja", + psbt=psbt, + ui_option=ui_option, + recipients_txt=recipients_txt, + labels=labels, + wallet_alias=wallet_alias, + wallet=wallet, + specter=app.specter, + rand=rand, + error=err, + ) + + +@app.route("/wallets//send/import") @login_required def wallet_importpsbt(wallet_alias): app.specter.check() @@ -1226,11 +1412,17 @@ def wallet_importpsbt(wallet_alias): app.logger.error("SpecterError while wallet_send: %s" % se) return render_template("base.jinja", error=se, specter=app.specter, rand=rand) err = None - return render_template("wallet/send/import/wallet_importpsbt.jinja", - wallet_alias=wallet_alias, wallet=wallet, - specter=app.specter, rand=rand, error=err) + return render_template( + "wallet/send/import/wallet_importpsbt.jinja", + wallet_alias=wallet_alias, + wallet=wallet, + specter=app.specter, + rand=rand, + error=err, + ) -@app.route('/wallets//send/pending/', methods=['GET', 'POST']) + +@app.route("/wallets//send/pending/", methods=["GET", "POST"]) @login_required def wallet_sendpending(wallet_alias): app.specter.check() @@ -1240,26 +1432,32 @@ def wallet_sendpending(wallet_alias): app.logger.error("SpecterError while wallet_sendpending: %s" % se) return render_template("base.jinja", error=se, specter=app.specter, rand=rand) if request.method == "POST": - action = request.form['action'] - if action == 'deletepsbt': + action = request.form["action"] + if action == "deletepsbt": try: - wallet.delete_pending_psbt(ast.literal_eval(request.form["pending_psbt"])["tx"]["txid"]) + wallet.delete_pending_psbt( + ast.literal_eval(request.form["pending_psbt"])["tx"]["txid"] + ) except Exception as e: app.logger.error("Could not delete Pending PSBT: %s" % e) flash("Could not delete Pending PSBT!", "error") pending_psbts = wallet.pending_psbts ######## Migration to multiple recipients format ############### for psbt in pending_psbts: - if not isinstance(pending_psbts[psbt]['address'], list): - pending_psbts[psbt]['address'] = [pending_psbts[psbt]['address']] - pending_psbts[psbt]['amount'] = [pending_psbts[psbt]['amount']] + if not isinstance(pending_psbts[psbt]["address"], list): + pending_psbts[psbt]["address"] = [pending_psbts[psbt]["address"]] + pending_psbts[psbt]["amount"] = [pending_psbts[psbt]["amount"]] ############################################################### - return render_template("wallet/send/pending/wallet_sendpending.jinja", pending_psbts=pending_psbts, - wallet_alias=wallet_alias, wallet=wallet, - specter=app.specter) + return render_template( + "wallet/send/pending/wallet_sendpending.jinja", + pending_psbts=pending_psbts, + wallet_alias=wallet_alias, + wallet=wallet, + specter=app.specter, + ) -@app.route('/wallets//settings/', methods=['GET','POST']) +@app.route("/wallets//settings/", methods=["GET", "POST"]) @login_required def wallet_settings(wallet_alias): app.specter.check() @@ -1270,9 +1468,9 @@ def wallet_settings(wallet_alias): app.logger.error("SpecterError while wallet_receive: %s" % se) return render_template("base.jinja", error=se, specter=app.specter, rand=rand) if request.method == "POST": - action = request.form['action'] + action = request.form["action"] if action == "rescanblockchain": - startblock = int(request.form['startblock']) + startblock = int(request.form["startblock"]) try: res = wallet.rpc.rescanblockchain(startblock, timeout=1) except requests.exceptions.ReadTimeout: @@ -1285,7 +1483,7 @@ def wallet_settings(wallet_alias): elif action == "abortrescan": res = wallet.rpc.abortrescan() if not res: - error="Failed to abort rescan. Maybe already complete?" + error = "Failed to abort rescan. Maybe already complete?" wallet.getdata() elif action == "rescanutxo": explorer = None @@ -1299,18 +1497,20 @@ def wallet_settings(wallet_alias): app.specter._info["utxorescan"] = None app.specter.utxorescanwallet = None elif action == "keypoolrefill": - delta = int(request.form['keypooladd']) + delta = int(request.form["keypooladd"]) wallet.keypoolrefill(wallet.keypool, wallet.keypool + delta) - wallet.keypoolrefill(wallet.change_keypool, wallet.change_keypool + delta, change=True) + wallet.keypoolrefill( + wallet.change_keypool, wallet.change_keypool + delta, change=True + ) wallet.getdata() elif action == "deletewallet": app.specter.wallet_manager.delete_wallet( wallet, app.specter.bitcoin_datadir, app.specter.chain ) - response = redirect(url_for('index')) + response = redirect(url_for("index")) return response elif action == "rename": - wallet_name = request.form['newtitle'] + wallet_name = request.form["newtitle"] if wallet_name in app.specter.wallet_manager.wallets_names: error = "Wallet already exists" else: @@ -1323,22 +1523,24 @@ def wallet_settings(wallet_alias): wallet=wallet, specter=app.specter, rand=rand, - error=error + error=error, ) else: return render_template( - "wallet/settings/wallet_settings.jinja", + "wallet/settings/wallet_settings.jinja", purposes=purposes, wallet_alias=wallet_alias, - wallet=wallet, + wallet=wallet, specter=app.specter, - rand=rand, - error=error + rand=rand, + error=error, ) + ################# devices management ##################### -@app.route('/new_device/', methods=['GET', 'POST']) + +@app.route("/new_device/", methods=["GET", "POST"]) @login_required def new_device(): app.specter.check() @@ -1348,49 +1550,66 @@ def new_device(): xpubs = "" strength = 128 mnemonic = generate_mnemonic(strength=strength) - if request.method == 'POST': - action = request.form['action'] - device_type = request.form['device_type'] - device_name = request.form['device_name'] + if request.method == "POST": + action = request.form["action"] + device_type = request.form["device_type"] + device_name = request.form["device_name"] if action == "newcolddevice": if not device_name: err = "Device name must not be empty" elif device_name in app.specter.device_manager.devices_names: err = "Device with this name already exists" - xpubs = request.form['xpubs'] + xpubs = request.form["xpubs"] if not xpubs: err = "xpubs name must not be empty" keys, failed = Key.parse_xpubs(xpubs) if len(failed) > 0: err = "Failed to parse these xpubs:\n" + "\n".join(failed) if err is None: - device = app.specter.device_manager.add_device(name=device_name, device_type=device_type, keys=keys) + device = app.specter.device_manager.add_device( + name=device_name, device_type=device_type, keys=keys + ) return redirect("/devices/%s/" % device.alias) elif action == "newhotdevice": if not device_name: err = "Device name must not be empty" elif device_name in app.specter.device_manager.devices_names: err = "Device with this name already exists" - if len(request.form['mnemonic'].split(' ')) not in [12, 15, 18, 21, 24]: + if len(request.form["mnemonic"].split(" ")) not in [12, 15, 18, 21, 24]: err = "Invalid mnemonic entered: Must contain either: 12, 15, 18, 21, or 24 words." - mnemo = Mnemonic('english') - if not mnemo.check(request.form['mnemonic']): + mnemo = Mnemonic("english") + if not mnemo.check(request.form["mnemonic"]): err = "Invalid mnemonic entered." if err is None: - mnemonic = request.form['mnemonic'] - passphrase = request.form['passphrase'] - device = app.specter.device_manager.add_device(name=device_name, device_type=device_type, keys=[]) - device.setup_device(mnemonic, passphrase, app.specter.wallet_manager, app.specter.chain != 'main') + mnemonic = request.form["mnemonic"] + passphrase = request.form["passphrase"] + device = app.specter.device_manager.add_device( + name=device_name, device_type=device_type, keys=[] + ) + device.setup_device( + mnemonic, + passphrase, + app.specter.wallet_manager, + app.specter.chain != "main", + ) return redirect("/devices/%s/" % device.alias) - elif action == 'generatemnemonic': - strength = int(request.form['strength']) + elif action == "generatemnemonic": + strength = int(request.form["strength"]) mnemonic = generate_mnemonic(strength=strength) - return render_template("device/new_device.jinja", device_type=device_type, - device_name=device_name, xpubs=xpubs, - mnemonic=mnemonic, strength=strength, - error=err, specter=app.specter, rand=rand) + return render_template( + "device/new_device.jinja", + device_type=device_type, + device_name=device_name, + xpubs=xpubs, + mnemonic=mnemonic, + strength=strength, + error=err, + specter=app.specter, + rand=rand, + ) -@app.route('/devices//', methods=['GET', 'POST']) + +@app.route("/devices//", methods=["GET", "POST"]) @login_required def device(device_alias): app.specter.check() @@ -1398,91 +1617,130 @@ def device(device_alias): try: device = app.specter.device_manager.get_by_alias(device_alias) except: - return render_template("base.jinja", error="Device not found", specter=app.specter, rand=rand) + return render_template( + "base.jinja", error="Device not found", specter=app.specter, rand=rand + ) wallets = device.wallets(app.specter.wallet_manager) - if request.method == 'POST': - action = request.form['action'] + if request.method == "POST": + action = request.form["action"] if action == "forget": if len(wallets) != 0: - err = "Device could not be removed since it is used in wallets: {}.\nYou must delete those wallets before you can remove this device.".format([wallet.name for wallet in wallets]) + err = "Device could not be removed since it is used in wallets: {}.\nYou must delete those wallets before you can remove this device.".format( + [wallet.name for wallet in wallets] + ) else: app.specter.device_manager.remove_device( device, app.specter.wallet_manager, bitcoin_datadir=app.specter.bitcoin_datadir, - chain=app.specter.chain + chain=app.specter.chain, ) return redirect("/") elif action == "delete_key": - key = request.form['key'] - device.remove_key(Key.from_json({ 'original': key })) + key = request.form["key"] + device.remove_key(Key.from_json({"original": key})) elif action == "add_keys": - return render_template("device/new_device.jinja", - device=device, device_alias=device_alias, specter=app.specter, rand=rand) + return render_template( + "device/new_device.jinja", + device=device, + device_alias=device_alias, + specter=app.specter, + rand=rand, + ) elif action == "morekeys": # refactor to fn - xpubs = request.form['xpubs'] + xpubs = request.form["xpubs"] keys, failed = Key.parse_xpubs(xpubs) err = None if len(failed) > 0: err = "Failed to parse these xpubs:\n" + "\n".join(failed) - return render_template("device/new_device.jinja", - device=device, device_alias=device_alias, xpubs=xpubs, error=err, specter=app.specter, rand=rand) + return render_template( + "device/new_device.jinja", + device=device, + device_alias=device_alias, + xpubs=xpubs, + error=err, + specter=app.specter, + rand=rand, + ) if err is None: device.add_keys(keys) elif action == "settype": - device_type = request.form['device_type'] + device_type = request.form["device_type"] device.set_type(device_type) device = copy.deepcopy(device) - device.keys.sort(key=lambda k: k.metadata["chain"] + k.metadata["purpose"], reverse=True) - return render_template("device/device.jinja", - device=device, device_alias=device_alias, purposes=purposes, wallets=wallets, error=err, specter=app.specter, rand=rand) + device.keys.sort( + key=lambda k: k.metadata["chain"] + k.metadata["purpose"], reverse=True + ) + return render_template( + "device/device.jinja", + device=device, + device_alias=device_alias, + purposes=purposes, + wallets=wallets, + error=err, + specter=app.specter, + rand=rand, + ) ############### filters ################## -@app.template_filter('datetime') + +@app.template_filter("datetime") def timedatetime(s): return format(datetime.fromtimestamp(s), "%d.%m.%Y %H:%M") -@app.template_filter('btcamount') +@app.template_filter("btcamount") def btcamount(value): - value = round(float(value),8) + value = round(float(value), 8) return "{:,.8f}".format(value).rstrip("0").rstrip(".") -@app.template_filter('btc2sat') + +@app.template_filter("btc2sat") def btc2sat(value): - value = int(round(float(value)*1e8)) + value = int(round(float(value) * 1e8)) return f"{value}" -@app.template_filter('feerate') + +@app.template_filter("feerate") def feerate(value): - value = float(value)*1e8 + value = float(value) * 1e8 return "{:,.2f}".format(value).rstrip("0").rstrip(".") -@app.template_filter('btcunitamount') + +@app.template_filter("btcunitamount") def btcunitamount(value): - if app.specter.unit != 'sat': + if app.specter.unit != "sat": return btcamount(value) value = float(value) return "{:,.0f}".format(round(value * 1e8)) -@app.template_filter('bytessize') +@app.template_filter("bytessize") def bytessize(value): value = float(value) - return '{:,.0f}'.format(value / float(1 << 30)) + " GB" + return "{:,.0f}".format(value / float(1 << 30)) + " GB" def notify_upgrade(): - ''' If a new version is available, notifies the user via flash - that there is an upgrade to specter.desktop - :return the current version - ''' - version_info={} - version_info["current"], version_info["latest"], version_info["upgrade"] = get_version_info() + """If a new version is available, notifies the user via flash + that there is an upgrade to specter.desktop + :return the current version + """ + version_info = {} + ( + version_info["current"], + version_info["latest"], + version_info["upgrade"], + ) = get_version_info() app.logger.info("Upgrade? {}".format(version_info["upgrade"])) if version_info["upgrade"]: - flash("There is a new version available. Consider strongly to upgrade to the new version {} with \"pip3 install cryptoadvance.specter --upgrade\"".format(version_info["latest"]), "info") + flash( + 'There is a new version available. Consider strongly to upgrade to the new version {} with "pip3 install cryptoadvance.specter --upgrade"'.format( + version_info["latest"] + ), + "info", + ) return version_info["current"] diff --git a/src/cryptoadvance/specter/device.py b/src/cryptoadvance/specter/device.py index a17c27d2f1..61ccb9dfc6 100644 --- a/src/cryptoadvance/specter/device.py +++ b/src/cryptoadvance/specter/device.py @@ -35,16 +35,13 @@ def create_psbts(self, base64_psbt, wallet): return {} @classmethod - def from_json(cls, device_dict, manager, - default_alias='', default_fullpath=''): - name = device_dict['name'] if 'name' in device_dict else '' - alias = (device_dict['alias'] - if 'alias' in device_dict - else default_alias) - keys = [Key.from_json(key_dict) for key_dict in device_dict['keys']] - fullpath = (device_dict['fullpath'] - if 'fullpath' in device_dict - else default_fullpath) + def from_json(cls, device_dict, manager, default_alias="", default_fullpath=""): + name = device_dict["name"] if "name" in device_dict else "" + alias = device_dict["alias"] if "alias" in device_dict else default_alias + keys = [Key.from_json(key_dict) for key_dict in device_dict["keys"]] + fullpath = ( + device_dict["fullpath"] if "fullpath" in device_dict else default_fullpath + ) return cls(name, alias, keys, fullpath, manager) @property @@ -61,7 +58,7 @@ def _update_keys(self): with fslock: with open(self.fullpath, "r") as f: content = json.load(f) - content['keys'] = [key.json for key in self.keys] + content["keys"] = [key.json for key in self.keys] with open(self.fullpath, "w") as f: json.dump(content, f, indent=4) self.manager.update() @@ -90,8 +87,8 @@ def set_type(self, device_type): json.dump(self.json, f, indent=4) self.manager.update() - def key_types(self, network='main'): - test = network != 'main' + def key_types(self, network="main"): + test = network != "main" return [key.key_type for key in self.keys if (key.is_testnet == test)] def __eq__(self, other): diff --git a/src/cryptoadvance/specter/device_manager.py b/src/cryptoadvance/specter/device_manager.py index 741e7b465c..1d5b3918a1 100644 --- a/src/cryptoadvance/specter/device_manager.py +++ b/src/cryptoadvance/specter/device_manager.py @@ -19,9 +19,10 @@ def get_device_class(device_type): class DeviceManager: - ''' A DeviceManager mainly manages the persistence of a device-json-structures - compliant to helper.load_jsons - ''' + """A DeviceManager mainly manages the persistence of a device-json-structures + compliant to helper.load_jsons + """ + # of them via json-files in an empty data folder def __init__(self, data_folder): self.update(data_folder=data_folder) @@ -38,11 +39,13 @@ def update(self, data_folder=None): devices_files = load_jsons(self.data_folder, key="name") for device_alias in devices_files: fullpath = os.path.join(self.data_folder, "%s.json" % device_alias) - devices[devices_files[device_alias]["name"]] = get_device_class(devices_files[device_alias]["type"]).from_json( + devices[devices_files[device_alias]["name"]] = get_device_class( + devices_files[device_alias]["type"] + ).from_json( devices_files[device_alias], self, default_alias=device_alias, - default_fullpath=fullpath + default_fullpath=fullpath, ) self.devices = devices @@ -83,11 +86,11 @@ def remove_device( device, wallet_manager=None, bitcoin_datadir=get_default_datadir(), - chain='main' + chain="main", ): os.remove(device.fullpath) # if device can delete itself - call it - if hasattr(device,'delete'): + if hasattr(device, "delete"): device.delete(wallet_manager, bitcoin_datadir=bitcoin_datadir, chain=chain) self.update() diff --git a/src/cryptoadvance/specter/devices/bitcoin_core.py b/src/cryptoadvance/specter/devices/bitcoin_core.py index 3a6f07cea1..50b9ac1f3e 100644 --- a/src/cryptoadvance/specter/devices/bitcoin_core.py +++ b/src/cryptoadvance/specter/devices/bitcoin_core.py @@ -21,68 +21,73 @@ class BitcoinCore(Device): def __init__(self, name, alias, keys, fullpath, manager): Device.__init__(self, name, alias, keys, fullpath, manager) - def setup_device(self, mnemonic, passphrase, - wallet_manager, testnet): + def setup_device(self, mnemonic, passphrase, wallet_manager, testnet): seed = Mnemonic.to_seed(mnemonic) xprv = seed_to_hd_master_key(seed, testnet=testnet) - wallet_name = os.path.join( - wallet_manager.rpc_path + '_hotstorage', self.alias) + wallet_name = os.path.join(wallet_manager.rpc_path + "_hotstorage", self.alias) wallet_manager.rpc.createwallet(wallet_name, False, True) rpc = wallet_manager.rpc.wallet(wallet_name) # TODO: Maybe more than 1000? Maybe add mechanism to add more later. # NOTE: This will work only on the network the device was added, # so hot devices should be filtered out by network. coin = int(testnet) - rpc.importmulti([ - { - 'desc': AddChecksum( - 'sh(wpkh({}/49h/{}h/0h/0/*))'.format(xprv, coin)), - 'range': 1000, - 'timestamp': 'now', - }, - { - 'desc': AddChecksum( - 'sh(wpkh({}/49h/{}h/0h/1/*))'.format(xprv, coin)), - 'range': 1000, - 'timestamp': 'now', - }, - { - 'desc': AddChecksum( - 'wpkh({}/84h/{}h/0h/0/*)'.format(xprv, coin)), - 'range': 1000, - 'timestamp': 'now', - }, - { - 'desc': AddChecksum( - 'wpkh({}/84h/{}h/0h/1/*)'.format(xprv, coin)), - 'range': 1000, - 'timestamp': 'now', - }, - { - 'desc': AddChecksum( - 'sh(wpkh({}/48h/{}h/0h/1h/0/*))'.format(xprv, coin)), - 'range': 1000, - 'timestamp': 'now', - }, - { - 'desc': AddChecksum( - 'sh(wpkh({}/48h/{}h/0h/1h/1/*))'.format(xprv, coin)), - 'range': 1000, - 'timestamp': 'now', - }, - { - 'desc': AddChecksum( - 'wpkh({}/48h/{}h/0h/2h/0/*)'.format(xprv, coin)), - 'range': 1000, - 'timestamp': 'now', - }, - { - 'desc': AddChecksum( - 'wpkh({}/48h/{}h/0h/2h/1/*)'.format(xprv, coin)), - 'range': 1000, - 'timestamp': 'now', - }, - ], {"rescan": False}) + rpc.importmulti( + [ + { + "desc": AddChecksum( + "sh(wpkh({}/49h/{}h/0h/0/*))".format(xprv, coin) + ), + "range": 1000, + "timestamp": "now", + }, + { + "desc": AddChecksum( + "sh(wpkh({}/49h/{}h/0h/1/*))".format(xprv, coin) + ), + "range": 1000, + "timestamp": "now", + }, + { + "desc": AddChecksum("wpkh({}/84h/{}h/0h/0/*)".format(xprv, coin)), + "range": 1000, + "timestamp": "now", + }, + { + "desc": AddChecksum("wpkh({}/84h/{}h/0h/1/*)".format(xprv, coin)), + "range": 1000, + "timestamp": "now", + }, + { + "desc": AddChecksum( + "sh(wpkh({}/48h/{}h/0h/1h/0/*))".format(xprv, coin) + ), + "range": 1000, + "timestamp": "now", + }, + { + "desc": AddChecksum( + "sh(wpkh({}/48h/{}h/0h/1h/1/*))".format(xprv, coin) + ), + "range": 1000, + "timestamp": "now", + }, + { + "desc": AddChecksum( + "wpkh({}/48h/{}h/0h/2h/0/*)".format(xprv, coin) + ), + "range": 1000, + "timestamp": "now", + }, + { + "desc": AddChecksum( + "wpkh({}/48h/{}h/0h/2h/1/*)".format(xprv, coin) + ), + "range": 1000, + "timestamp": "now", + }, + ], + {"rescan": False}, + ) if passphrase: rpc.encryptwallet(passphrase) @@ -101,36 +106,36 @@ def setup_device(self, mnemonic, passphrase, if not testnet: # Nested Segwit xpub = xpubs[1] - ypub = convert_xpub_prefix(xpub, b'\x04\x9d\x7c\xb2') + ypub = convert_xpub_prefix(xpub, b"\x04\x9d\x7c\xb2") xpubs_str += "[%s/49'/0'/0']%s\n" % (master_fpr, ypub) # native Segwit xpub = xpubs[2] - zpub = convert_xpub_prefix(xpub, b'\x04\xb2\x47\x46') + zpub = convert_xpub_prefix(xpub, b"\x04\xb2\x47\x46") xpubs_str += "[%s/84'/0'/0']%s\n" % (master_fpr, zpub) # Multisig nested Segwit xpub = xpubs[3] - Ypub = convert_xpub_prefix(xpub, b'\x02\x95\xb4\x3f') + Ypub = convert_xpub_prefix(xpub, b"\x02\x95\xb4\x3f") xpubs_str += "[%s/48'/0'/0'/1']%s\n" % (master_fpr, Ypub) # Multisig native Segwit xpub = xpubs[4] - Zpub = convert_xpub_prefix(xpub, b'\x02\xaa\x7e\xd3') + Zpub = convert_xpub_prefix(xpub, b"\x02\xaa\x7e\xd3") xpubs_str += "[%s/48'/0'/0'/2']%s\n" % (master_fpr, Zpub) else: # Testnet nested Segwit xpub = xpubs[1] - upub = convert_xpub_prefix(xpub, b'\x04\x4a\x52\x62') + upub = convert_xpub_prefix(xpub, b"\x04\x4a\x52\x62") xpubs_str += "[%s/49'/1'/0']%s\n" % (master_fpr, upub) # Testnet native Segwit xpub = xpubs[2] - vpub = convert_xpub_prefix(xpub, b'\x04\x5f\x1c\xf6') + vpub = convert_xpub_prefix(xpub, b"\x04\x5f\x1c\xf6") xpubs_str += "[%s/84'/1'/0']%s\n" % (master_fpr, vpub) # Testnet multisig nested Segwit xpub = xpubs[3] - Upub = convert_xpub_prefix(xpub, b'\x02\x42\x89\xef') + Upub = convert_xpub_prefix(xpub, b"\x02\x42\x89\xef") xpubs_str += "[%s/48'/1'/0'/1']%s\n" % (master_fpr, Upub) # Testnet multisig native Segwit xpub = xpubs[4] - Vpub = convert_xpub_prefix(xpub, b'\x02\x57\x54\x83') + Vpub = convert_xpub_prefix(xpub, b"\x02\x57\x54\x83") xpubs_str += "[%s/48'/1'/0'/2']%s\n" % (master_fpr, Vpub) keys, failed = Key.parse_xpubs(xpubs_str) @@ -138,50 +143,47 @@ def setup_device(self, mnemonic, passphrase, # TODO: This should never occur, but just in case, # we must make sure to catch it properly so it # doesn't crash the app no matter what. - raise Exception( - "Failed to parse these xpubs:\n" + "\n".join(failed)) + raise Exception("Failed to parse these xpubs:\n" + "\n".join(failed)) else: self.add_keys(keys) def _load_wallet(self, wallet_manager): try: - existing_wallets = [w["name"] - for w - in wallet_manager.rpc.listwalletdir()["wallets"]] + existing_wallets = [ + w["name"] for w in wallet_manager.rpc.listwalletdir()["wallets"] + ] except: existing_wallets = None loaded_wallets = wallet_manager.rpc.listwallets() hotstorage_path = wallet_manager.rpc_path + "_hotstorage" - if (existing_wallets is None or - os.path.join(hotstorage_path, self.alias) in existing_wallets): + if ( + existing_wallets is None + or os.path.join(hotstorage_path, self.alias) in existing_wallets + ): if os.path.join(hotstorage_path, self.alias) not in loaded_wallets: - wallet_manager.rpc.loadwallet(os.path.join( - hotstorage_path, self.alias)) + wallet_manager.rpc.loadwallet(os.path.join(hotstorage_path, self.alias)) def create_psbts(self, base64_psbt, wallet): - return {'core': base64_psbt} + return {"core": base64_psbt} def sign_psbt(self, base64_psbt, wallet, passphrase): # Load the wallet if not loaded self._load_wallet(wallet.manager) - rpc = wallet.manager.rpc.wallet(os.path.join( - wallet.manager.rpc_path + "_hotstorage", self.alias)) + rpc = wallet.manager.rpc.wallet( + os.path.join(wallet.manager.rpc_path + "_hotstorage", self.alias) + ) if passphrase: rpc.walletpassphrase(passphrase, 60) signed_psbt = rpc.walletprocesspsbt(base64_psbt) - if base64_psbt == signed_psbt['psbt']: - raise Exception( - 'Make sure you have entered the passphrase correctly.') + if base64_psbt == signed_psbt["psbt"]: + raise Exception("Make sure you have entered the passphrase correctly.") if passphrase: rpc.walletlock() return signed_psbt def delete( - self, - wallet_manager, - bitcoin_datadir=get_default_datadir(), - chain = 'main' + self, wallet_manager, bitcoin_datadir=get_default_datadir(), chain="main" ): try: wallet_rpc_path = os.path.join( @@ -190,7 +192,7 @@ def delete( wallet_manager.rpc.unloadwallet(wallet_rpc_path) # Try deleting wallet file if bitcoin_datadir: - if chain != 'main': + if chain != "main": bitcoin_datadir = os.path.join(bitcoin_datadir, chain) candidates = [ os.path.join(bitcoin_datadir, wallet_rpc_path), @@ -203,6 +205,7 @@ def delete( except: pass # We tried... + # We need to copy it like this because HWI uses it as a dependency, # but requires v0.18 which doesn't have this function. @@ -213,7 +216,7 @@ def seed_to_hd_master_key(seed, testnet=False) -> str: raise ValueError("Provided seed should be between 16 and 64 bytes") # Compute HMAC-SHA512 of seed - seed = hmac.new(b"Bitcoin seed", seed, digestmod='sha512').digest() + seed = hmac.new(b"Bitcoin seed", seed, digestmod="sha512").digest() # Serialization format can be found at: # https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#Serialization_format @@ -261,7 +264,7 @@ def derive_xpubs_from_xprv(xprv, paths: list, rpc): def swap_fingerprint(xpub, fingerprint): """Replaces fingerprint in xpub""" raw = decode_base58(xpub) - swapped = raw[:5]+fingerprint+raw[9:] + swapped = raw[:5] + fingerprint + raw[9:] return encode_base58_checksum(swapped) @@ -280,26 +283,25 @@ def get_child(xprv, index): version = stream.read(4) depth = stream.read(1)[0] fingerprint = stream.read(4) - child_number = int.from_bytes(stream.read(4), 'big') + child_number = int.from_bytes(stream.read(4), "big") chain_code = stream.read(32) stream.read(1) secret = stream.read(32) - data = b'\x00' + secret + index.to_bytes(4, 'big') - raw = hmac.new(chain_code, data, digestmod='sha512').digest() + data = b"\x00" + secret + index.to_bytes(4, "big") + raw = hmac.new(chain_code, data, digestmod="sha512").digest() tweak = raw[:32] chain_code = raw[32:] - new_secret = (int.from_bytes(secret, 'big') + - int.from_bytes(tweak, 'big')) % N - res = version+bytes([depth+1])+fingerprint+index.to_bytes(4, 'big') - res += chain_code+b"\x00"+new_secret.to_bytes(32, 'big') + new_secret = (int.from_bytes(secret, "big") + int.from_bytes(tweak, "big")) % N + res = version + bytes([depth + 1]) + fingerprint + index.to_bytes(4, "big") + res += chain_code + b"\x00" + new_secret.to_bytes(32, "big") return encode_base58_checksum(res) def parse_path(path: str) -> list: """ - Converts derivation path of the form + Converts derivation path of the form m/44h/1'/0'/0/32 to int array """ arr = path.split("/") @@ -312,7 +314,7 @@ def parse_path(path: str) -> list: arr = arr[:-1] for i, e in enumerate(arr): if e[-1] == "h" or e[-1] == "'": - arr[i] = int(e[:-1])+0x80000000 + arr[i] = int(e[:-1]) + 0x80000000 else: arr[i] = int(e) return arr diff --git a/src/cryptoadvance/specter/devices/cobo.py b/src/cryptoadvance/specter/devices/cobo.py index 04306e5df9..b21a533568 100644 --- a/src/cryptoadvance/specter/devices/cobo.py +++ b/src/cryptoadvance/specter/devices/cobo.py @@ -1,4 +1,5 @@ import hashlib + # from ..device import Device from .sd_card_device import SDCardDevice from hwilib.serializations import PSBT @@ -16,7 +17,7 @@ class Cobo(SDCardDevice): sd_card_support = True qr_code_support = True exportable_to_wallet = True - wallet_export_type = 'qr' + wallet_export_type = "qr" def __init__(self, name, alias, keys, fullpath, manager): super().__init__(name, alias, keys, fullpath, manager) @@ -28,22 +29,18 @@ def create_psbts(self, base64_psbt, wallet): raw_psbt = a2b_base64(updated_psbt) enc, hsh = bcur.bcur_encode(raw_psbt) qrpsbt = ("ur:bytes/%s/%s" % (hsh, enc)).upper() - psbts['qrcode'] = qrpsbt + psbts["qrcode"] = qrpsbt return psbts def export_wallet(self, wallet): # Cobo uses ColdCard's style - CC_TYPES = { - 'legacy': 'BIP45', - 'p2sh-segwit': 'P2WSH-P2SH', - 'bech32': 'P2WSH' - } + CC_TYPES = {"legacy": "BIP45", "p2sh-segwit": "P2WSH-P2SH", "bech32": "P2WSH"} # try to find at least one derivation # cc assume the same derivation for all keys :( derivation = None # find correct key for k in wallet.keys: - if k in self.keys and k.derivation != '': + if k in self.keys and k.derivation != "": derivation = k.derivation.replace("h", "'") break if derivation is None: @@ -53,14 +50,17 @@ def export_wallet(self, wallet): Policy: {} of {} Derivation: {} Format: {} -""".format(wallet.name, wallet.sigs_required, - len(wallet.keys), derivation, - CC_TYPES[wallet.address_type] - ) +""".format( + wallet.name, + wallet.sigs_required, + len(wallet.keys), + derivation, + CC_TYPES[wallet.address_type], + ) for k in wallet.keys: # cc assumes fingerprint is known fingerprint = k.fingerprint - if fingerprint == '': + if fingerprint == "": fingerprint = get_xpub_fingerprint(k.xpub).hex() cc_file += "{}: {}\n".format(fingerprint.upper(), k.xpub) enc, hsh = bcur.bcur_encode(cc_file.encode()) diff --git a/src/cryptoadvance/specter/devices/coldcard.py b/src/cryptoadvance/specter/devices/coldcard.py index 4f370931b3..1298f080c2 100644 --- a/src/cryptoadvance/specter/devices/coldcard.py +++ b/src/cryptoadvance/specter/devices/coldcard.py @@ -3,11 +3,7 @@ from ..util.xpub import get_xpub_fingerprint -CC_TYPES = { - 'legacy': 'BIP45', - 'p2sh-segwit': 'P2WSH-P2SH', - 'bech32': 'P2WSH' -} +CC_TYPES = {"legacy": "BIP45", "p2sh-segwit": "P2WSH-P2SH", "bech32": "P2WSH"} class ColdCard(SDCardDevice): @@ -15,7 +11,7 @@ class ColdCard(SDCardDevice): name = "ColdCard" sd_card_support = True - wallet_export_type = 'file' + wallet_export_type = "file" supports_hwi_multisig_display_address = True def __init__(self, name, alias, keys, fullpath, manager): @@ -26,17 +22,13 @@ def create_psbts(self, base64_psbt, wallet): return psbts def export_wallet(self, wallet): - CC_TYPES = { - 'legacy': 'BIP45', - 'p2sh-segwit': 'P2WSH-P2SH', - 'bech32': 'P2WSH' - } + CC_TYPES = {"legacy": "BIP45", "p2sh-segwit": "P2WSH-P2SH", "bech32": "P2WSH"} # try to find at least one derivation # cc assume the same derivation for all keys :( derivation = None # find correct key for k in wallet.keys: - if k in self.keys and k.derivation != '': + if k in self.keys and k.derivation != "": derivation = k.derivation.replace("h", "'") break if derivation is None: @@ -47,14 +39,17 @@ def export_wallet(self, wallet): Policy: {} of {} Derivation: {} Format: {} -""".format(wallet.name, wallet.sigs_required, - len(wallet.keys), derivation, - CC_TYPES[wallet.address_type] - ) +""".format( + wallet.name, + wallet.sigs_required, + len(wallet.keys), + derivation, + CC_TYPES[wallet.address_type], + ) for k in wallet.keys: # cc assumes fingerprint is known fingerprint = k.fingerprint - if fingerprint == '': + if fingerprint == "": fingerprint = get_xpub_fingerprint(k.xpub).hex() cc_file += "{}: {}\n".format(fingerprint.upper(), k.xpub) return urllib.parse.quote(cc_file) diff --git a/src/cryptoadvance/specter/devices/electrum.py b/src/cryptoadvance/specter/devices/electrum.py index 4ba46e61d1..0fd67b2047 100644 --- a/src/cryptoadvance/specter/devices/electrum.py +++ b/src/cryptoadvance/specter/devices/electrum.py @@ -18,8 +18,7 @@ def create_psbts(self, base64_psbt, wallet): # remove non_witness utxo for QR code updated_psbt = wallet.fill_psbt(base64_psbt, non_witness=False, xpubs=False) psbts = { - 'qrcode': b43_encode(a2b_base64(updated_psbt)), - 'sdcard': base64_psbt, + "qrcode": b43_encode(a2b_base64(updated_psbt)), + "sdcard": base64_psbt, } return psbts - diff --git a/src/cryptoadvance/specter/devices/generic.py b/src/cryptoadvance/specter/devices/generic.py index 7b8606b2e4..3234b98ccd 100644 --- a/src/cryptoadvance/specter/devices/generic.py +++ b/src/cryptoadvance/specter/devices/generic.py @@ -13,7 +13,7 @@ def __init__(self, name, alias, keys, fullpath, manager): def create_psbts(self, base64_psbt, wallet): psbts = { - 'qrcode': base64_psbt, - 'sdcard': base64_psbt, + "qrcode": base64_psbt, + "sdcard": base64_psbt, } return psbts diff --git a/src/cryptoadvance/specter/devices/hwi/bitbox02.py b/src/cryptoadvance/specter/devices/hwi/bitbox02.py index f127de17b5..e8dc7076a1 100644 --- a/src/cryptoadvance/specter/devices/hwi/bitbox02.py +++ b/src/cryptoadvance/specter/devices/hwi/bitbox02.py @@ -107,7 +107,7 @@ def attestation_check(self, result: bool) -> None: class CLINoiseConfig(util.BitBoxAppNoiseConfig): - """ Noise pairing and attestation check handling in the terminal (stdin/stdout) """ + """Noise pairing and attestation check handling in the terminal (stdin/stdout)""" def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool: if _using_external_gui: @@ -225,7 +225,7 @@ def bitbox02_exception(f: T) -> T: @wraps(f) def func(*args, **kwargs): # type: ignore - """ Wraps f, mapping exceptions. """ + """Wraps f, mapping exceptions.""" try: return f(*args, **kwargs) except UserAbortException: diff --git a/src/cryptoadvance/specter/devices/hwi/keepkey.py b/src/cryptoadvance/specter/devices/hwi/keepkey.py index 149021ace9..d75c3db735 100644 --- a/src/cryptoadvance/specter/devices/hwi/keepkey.py +++ b/src/cryptoadvance/specter/devices/hwi/keepkey.py @@ -1,6 +1,7 @@ from .trezor import TrezorClient + class KeepkeyClient(TrezorClient): - def __init__(self, path, password='', expert=False): + def __init__(self, path, password="", expert=False): super(KeepkeyClient, self).__init__(path, password, expert) - self.type = 'Keepkey' \ No newline at end of file + self.type = "Keepkey" diff --git a/src/cryptoadvance/specter/devices/hwi/specter_diy.py b/src/cryptoadvance/specter/devices/hwi/specter_diy.py index 9b4a26b147..956659291d 100644 --- a/src/cryptoadvance/specter/devices/hwi/specter_diy.py +++ b/src/cryptoadvance/specter/devices/hwi/specter_diy.py @@ -3,9 +3,13 @@ from hwilib.serializations import PSBT from hwilib.hwwclient import HardwareWalletClient -from hwilib.errors import (ActionCanceledError, BadArgumentError, - DeviceBusyError, DeviceFailureError, - UnavailableActionError) +from hwilib.errors import ( + ActionCanceledError, + BadArgumentError, + DeviceBusyError, + DeviceFailureError, + UnavailableActionError, +) from hwilib.base58 import xpub_main_2_test from hwilib import base58 @@ -13,23 +17,26 @@ import serial.tools.list_ports import socket, time + class SpecterClient(HardwareWalletClient): """Create a client for a HID device that has already been opened. This abstract class defines the methods that hardware wallet subclasses should implement. """ + # timeout large enough to handle xpub derivations TIMEOUT = 3 - def __init__(self, path: str, password:str="", expert:bool=False) -> None: + + def __init__(self, path: str, password: str = "", expert: bool = False) -> None: super().__init__(path, password, expert) - self.simulator = (":" in path) + self.simulator = ":" in path if self.simulator: self.dev = SpecterSimulator(path) else: self.dev = SpecterUSBDevice(path) - def query(self, data: str, timeout:Optional[float] = None) -> str: + def query(self, data: str, timeout: Optional[float] = None) -> str: """Send a text-based query to the device and get back the response""" res = self.dev.query(data, timeout) if res == "error: User cancelled": @@ -51,12 +58,12 @@ def get_pubkey_at_path(self, bip32_path: str) -> Dict[str, str]: """ # this should be fast xpub = self.query("xpub %s" % bip32_path, timeout=self.TIMEOUT) - # Specter returns xpub with a prefix + # Specter returns xpub with a prefix # for a network currently selected on the device if self.is_testnet: - return {'xpub': xpub_main_2_test(xpub)} + return {"xpub": xpub_main_2_test(xpub)} else: - return {'xpub': xpub_test_2_main(xpub)} + return {"xpub": xpub_test_2_main(xpub)} def sign_tx(self, psbt: PSBT) -> Dict[str, str]: """Sign a partially signed bitcoin transaction (PSBT). @@ -65,7 +72,7 @@ def sign_tx(self, psbt: PSBT) -> Dict[str, str]: """ # this one can hang for quite some time signed_tx = self.query("sign %s" % psbt.serialize()) - return {'psbt': signed_tx} + return {"psbt": signed_tx} def sign_message(self, message: str, bip32_path: str) -> Dict[str, str]: """Sign a message (bitcoin message signing). @@ -76,7 +83,7 @@ def sign_message(self, message: str, bip32_path: str) -> Dict[str, str]: Return {"signature": }. """ - sig = self.query('signmessage %s %s' % (bip32_path, message)) + sig = self.query("signmessage %s %s" % (bip32_path, message)) return {"signature": sig} def display_address( @@ -105,7 +112,7 @@ def display_address( if redeem_script is not None: request += f" {redeem_script}" address = self.query(request) - return {'address': address} + return {"address": address} def wipe_device(self) -> Dict[str, Union[bool, str, int]]: """Wipe the HID device. @@ -116,8 +123,9 @@ def wipe_device(self) -> Dict[str, Union[bool, str, int]]: Raise UnavailableActionError if appropriate for the device. """ - raise NotImplementedError("The SpecterClient class " - "does not implement this method") + raise NotImplementedError( + "The SpecterClient class " "does not implement this method" + ) def setup_device( self, label: str = "", passphrase: str = "" @@ -130,8 +138,9 @@ def setup_device( Raise UnavailableActionError if appropriate for the device. """ - raise NotImplementedError("The SpecterClient class " - "does not implement this method") + raise NotImplementedError( + "The SpecterClient class " "does not implement this method" + ) def restore_device( self, label: str = "", word_count: int = 24 @@ -144,8 +153,9 @@ def restore_device( Raise UnavailableActionError if appropriate for the device. """ - raise NotImplementedError("The SpecterClient class " - "does not implement this method") + raise NotImplementedError( + "The SpecterClient class " "does not implement this method" + ) def backup_device( self, label: str = "", passphrase: str = "" @@ -158,8 +168,9 @@ def backup_device( Raise UnavailableActionError if appropriate for the device. """ - raise NotImplementedError("The SpecterClient class " - "does not implement this method") + raise NotImplementedError( + "The SpecterClient class " "does not implement this method" + ) def close(self) -> None: """Close the device.""" @@ -175,8 +186,9 @@ def prompt_pin(self) -> Dict[str, Union[bool, str, int]]: Raise UnavailableActionError if appropriate for the device. """ - raise NotImplementedError("The SpecterClient class " - "does not implement this method") + raise NotImplementedError( + "The SpecterClient class " "does not implement this method" + ) def send_pin(self) -> Dict[str, Union[bool, str, int]]: """Send PIN. @@ -187,8 +199,9 @@ def send_pin(self) -> Dict[str, Union[bool, str, int]]: Raise UnavailableActionError if appropriate for the device. """ - raise NotImplementedError("The SpecterClient class " - "does not implement this method") + raise NotImplementedError( + "The SpecterClient class " "does not implement this method" + ) def toggle_passphrase(self) -> Dict[str, Union[bool, str, int]]: """Toggle passphrase. @@ -199,32 +212,35 @@ def toggle_passphrase(self) -> Dict[str, Union[bool, str, int]]: Raise UnavailableActionError if appropriate for the device. """ - raise NotImplementedError("The SpecterClient class " - "does not implement this method") + raise NotImplementedError( + "The SpecterClient class " "does not implement this method" + ) ############ extra functions Specter supports ############ - def get_random(self, num_bytes:int=32): + def get_random(self, num_bytes: int = 32): if num_bytes < 0 or num_bytes > 10000: raise BadArgumentError("We can only get up to 10k bytes of random data") res = self.query("getrandom %d" % num_bytes) return bytes.fromhex(res) - def import_wallet(self, name:str, descriptor:str): + def import_wallet(self, name: str, descriptor: str): # TODO: implement pass -def enumerate(password=''): +def enumerate(password=""): """ - Returns a list of detected Specter devices + Returns a list of detected Specter devices with their fingerprints and client's paths """ results = [] # find ports with micropython's VID - ports = [port.device for port - in serial.tools.list_ports.comports() - if is_micropython(port)] + ports = [ + port.device + for port in serial.tools.list_ports.comports() + if is_micropython(port) + ] try: # check if there is a simulator on port 8789 # and we can connect to it @@ -240,48 +256,56 @@ def enumerate(password=''): try: path = port data = { - 'type': 'specter', - 'model': 'specter-diy', - 'path': path, - 'needs_passphrase': False + "type": "specter", + "model": "specter-diy", + "path": path, + "needs_passphrase": False, } client = SpecterClient(path) - data['fingerprint'] = client.get_master_fingerprint_hex() + data["fingerprint"] = client.get_master_fingerprint_hex() client.close() results.append(data) except: pass return results + ############# Helper functions and base classes ############## + def xpub_test_2_main(xpub: str) -> str: data = base58.decode(xpub) - main_data = b'\x04\x88\xb2\x1e' + data[4:-4] + main_data = b"\x04\x88\xb2\x1e" + data[4:-4] checksum = base58.hash256(main_data)[0:4] return base58.encode(main_data + checksum) + def is_micropython(port): return "VID:PID=F055:" in port.hwid.upper() + class SpecterBase: """Class with common constants and command encoding""" + EOL = b"\r\n" ACK = b"ACK" ACK_TIMOUT = 1 + def prepare_cmd(self, data): """ Prepends command with 2*EOL and appends EOL at the end. Double EOL in the beginning makes sure all pending data will be cleaned up. """ - return self.EOL*2 + data.encode('utf-8') + self.EOL + return self.EOL * 2 + data.encode("utf-8") + self.EOL + class SpecterUSBDevice(SpecterBase): """ Base class for USB device. Implements a simple query command over serial """ + def __init__(self, path): self.ser = serial.Serial(baudrate=115200, timeout=30) self.ser.port = path @@ -295,7 +319,7 @@ def read_until(self, eol, timeout=None): res += raw except Exception as e: time.sleep(0.01) - if timeout is not None and time.time() > t0+timeout: + if timeout is not None and time.time() > t0 + timeout: self.ser.close() raise DeviceBusyError("Timeout") return res @@ -306,20 +330,22 @@ def query(self, data, timeout=None): self.ser.open() self.ser.write(self.prepare_cmd(data)) # first we should get ACK - res = self.read_until(self.EOL, self.ACK_TIMOUT)[:-len(self.EOL)] + res = self.read_until(self.EOL, self.ACK_TIMOUT)[: -len(self.EOL)] # then we should get the data itself if res != self.ACK: self.ser.close() raise DeviceBusyError("Device didn't return ACK") - res = self.read_until(self.EOL, timeout)[:-len(self.EOL)] + res = self.read_until(self.EOL, timeout)[: -len(self.EOL)] self.ser.close() return res.decode() + class SpecterSimulator(SpecterBase): """ Base class for the simulator. Implements a simple query command over tcp/ip socket """ + def __init__(self, path): arr = path.split(":") self.sock_settings = (arr[0], int(arr[1])) @@ -333,7 +359,7 @@ def read_until(self, s, eol, timeout=None): res += raw except Exception as e: time.sleep(0.01) - if timeout is not None and time.time() > t0+timeout: + if timeout is not None and time.time() > t0 + timeout: s.close() raise DeviceBusyError("Timeout") return res @@ -344,10 +370,10 @@ def query(self, data, timeout=None): s.send(self.prepare_cmd(data)) s.setblocking(False) # we will get ACK right away - res = self.read_until(s, self.EOL, self.ACK_TIMOUT)[:-len(self.EOL)] + res = self.read_until(s, self.EOL, self.ACK_TIMOUT)[: -len(self.EOL)] if res != self.ACK: raise DeviceBusyError("Device didn't return ACK") # fetch with required timeout - res = self.read_until(s, self.EOL, timeout)[:-len(self.EOL)] + res = self.read_until(s, self.EOL, timeout)[: -len(self.EOL)] s.close() return res.decode() diff --git a/src/cryptoadvance/specter/devices/hwi/trezor.py b/src/cryptoadvance/specter/devices/hwi/trezor.py index 70ae0daff1..9f939a864a 100644 --- a/src/cryptoadvance/specter/devices/hwi/trezor.py +++ b/src/cryptoadvance/specter/devices/hwi/trezor.py @@ -62,7 +62,7 @@ import sys import struct -py_enumerate = enumerate # Need to use the enumerate built-in but there's another function already named that +py_enumerate = enumerate # Need to use the enumerate built-in but there's another function already named that # Only handles up to 15 of 15 def parse_multisig(script): @@ -81,10 +81,16 @@ def parse_multisig(script): if pubkey_len != 33: break offset += 1 - key = script[offset:offset + 33] + key = script[offset : offset + 33] offset += 33 - hd_node = proto.HDNodeType(depth=0, fingerprint=0, child_num=0, chain_code=b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', public_key=key) + hd_node = proto.HDNodeType( + depth=0, + fingerprint=0, + child_num=0, + chain_code=b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + public_key=key, + ) pubkeys.append(proto.HDNodePathType(node=hd_node, address_n=[])) # Check things at the end @@ -97,9 +103,12 @@ def parse_multisig(script): return (False, None) # Build MultisigRedeemScriptType and return it - multisig = proto.MultisigRedeemScriptType(m=m, signatures=[b''] * n, pubkeys=pubkeys) + multisig = proto.MultisigRedeemScriptType( + m=m, signatures=[b""] * n, pubkeys=pubkeys + ) return (True, multisig) + # Parses the PSBT_GLOBAL_XPUB fields of a PSBT as multisig pubkeys def parse_multisig_xpubs(tx, psbt_in_out, multisig): try: @@ -107,7 +116,10 @@ def parse_multisig_xpubs(tx, psbt_in_out, multisig): xpubs = [xpub for xpub in tx.unknown.keys() if xpub.startswith(b"\x01")] derivations = [tx.unknown[xpub] for xpub in xpubs] # unpack - derivations = [list(struct.unpack("<" + "I" * (len(value) // 4), value)) for value in derivations] + derivations = [ + list(struct.unpack("<" + "I" * (len(value) // 4), value)) + for value in derivations + ] new_pubs = [] for pub in old_pubs: # derivation @@ -121,17 +133,26 @@ def parse_multisig_xpubs(tx, psbt_in_out, multisig): return multisig break xpub = xpubs[idx][1:] - address_n = der[len(derivations[idx]):] + address_n = der[len(derivations[idx]) :] xpub_obj = ExtendedKey() xpub_obj.deserialize(base58_encode(xpub + hash256(xpub)[:4])) - hd_node = proto.HDNodeType(depth=xpub_obj.depth, fingerprint=der[0], child_num=xpub_obj.child_num, chain_code=xpub_obj.chaincode, public_key=xpub_obj.pubkey) + hd_node = proto.HDNodeType( + depth=xpub_obj.depth, + fingerprint=der[0], + child_num=xpub_obj.child_num, + chain_code=xpub_obj.chaincode, + public_key=xpub_obj.pubkey, + ) new_pub = proto.HDNodePathType(node=hd_node, address_n=address_n) new_pubs.append(new_pub) - return proto.MultisigRedeemScriptType(m=multisig.m, signatures=multisig.signatures, pubkeys=new_pubs) + return proto.MultisigRedeemScriptType( + m=multisig.m, signatures=multisig.signatures, pubkeys=new_pubs + ) except: # If not all necessary data is available or malformatted, return the original multisig return multisig + def trezor_exception(f): def func(*args, **kwargs): try: @@ -139,11 +160,13 @@ def func(*args, **kwargs): except ValueError as e: raise BadArgumentError(str(e)) except Cancelled: - raise ActionCanceledError('{} canceled'.format(f.__name__)) + raise ActionCanceledError("{} canceled".format(f.__name__)) except USBErrorNoDevice: - raise DeviceConnectionError('Device disconnected') + raise DeviceConnectionError("Device disconnected") + return func + def interactive_get_pin(self, code=None): if code == PIN_CURRENT: desc = "current PIN" @@ -163,35 +186,41 @@ def interactive_get_pin(self, code=None): else: return pin + # This class extends the HardwareWalletClient for Trezor specific things class TrezorClient(HardwareWalletClient): - - def __init__(self, path, password='', expert=False): + def __init__(self, path, password="", expert=False): super(TrezorClient, self).__init__(path, password, expert) self.simulator = False - if path.startswith('udp'): - logging.debug('Simulator found, using DebugLink') + if path.startswith("udp"): + logging.debug("Simulator found, using DebugLink") transport = get_transport(path) self.client = TrezorClientDebugLink(transport=transport) self.simulator = True self.client.set_passphrase(password) else: - self.client = Trezor(transport=get_transport(path), ui=PassphraseUI(password)) + self.client = Trezor( + transport=get_transport(path), ui=PassphraseUI(password) + ) # if it wasn't able to find a client, throw an error if not self.client: raise IOError("no Device") self.password = password - self.type = 'Trezor' + self.type = "Trezor" def _check_unlocked(self): - self.coin_name = 'Testnet' if self.is_testnet else 'Bitcoin' + self.coin_name = "Testnet" if self.is_testnet else "Bitcoin" self.client.init_device() - if self.client.features.model == 'T': + if self.client.features.model == "T": self.client.ui.disallow_passphrase() if self.client.features.pin_protection and not self.client.features.pin_cached: - raise DeviceNotReadyError('{} is locked. Unlock by using \'promptpin\' and then \'sendpin\'.'.format(self.type)) + raise DeviceNotReadyError( + "{} is locked. Unlock by using 'promptpin' and then 'sendpin'.".format( + self.type + ) + ) # Must return a dict with the xpub # Retrieves the public key at the specified BIP 32 derivation path @@ -202,11 +231,13 @@ def get_pubkey_at_path(self, path): expanded_path = tools.parse_path(path) except ValueError as e: raise BadArgumentError(str(e)) - output = btc.get_public_node(self.client, expanded_path, coin_name=self.coin_name) + output = btc.get_public_node( + self.client, expanded_path, coin_name=self.coin_name + ) if self.is_testnet: - result = {'xpub': xpub_main_2_test(output.xpub)} + result = {"xpub": xpub_main_2_test(output.xpub)} else: - result = {'xpub': output.xpub} + result = {"xpub": output.xpub} if self.expert: xpub_obj = ExtendedKey() xpub_obj.deserialize(output.xpub) @@ -220,7 +251,7 @@ def sign_tx(self, tx): self._check_unlocked() # Get this devices master key fingerprint - master_key = btc.get_public_node(self.client, [0x80000000], coin_name='Bitcoin') + master_key = btc.get_public_node(self.client, [0x80000000], coin_name="Bitcoin") master_fp = get_xpub_fingerprint(master_key.xpub) # Do multiple passes for multisig @@ -230,8 +261,12 @@ def sign_tx(self, tx): while p < passes: # Prepare inputs inputs = [] - to_ignore = [] # Note down which inputs whose signatures we're going to ignore - for input_num, (psbt_in, txin) in py_enumerate(list(zip(tx.inputs, tx.tx.vin))): + to_ignore = ( + [] + ) # Note down which inputs whose signatures we're going to ignore + for input_num, (psbt_in, txin) in py_enumerate( + list(zip(tx.inputs, tx.tx.vin)) + ): txinputtype = proto.TxInputType() # Set the input stuff @@ -240,13 +275,17 @@ def sign_tx(self, tx): txinputtype.sequence = txin.nSequence # Detrermine spend type - scriptcode = b'' + scriptcode = b"" utxo = None if psbt_in.witness_utxo: utxo = psbt_in.witness_utxo if psbt_in.non_witness_utxo: if txin.prevout.hash != psbt_in.non_witness_utxo.sha256: - raise BadArgumentError('Input {} has a non_witness_utxo with the wrong hash'.format(input_num)) + raise BadArgumentError( + "Input {} has a non_witness_utxo with the wrong hash".format( + input_num + ) + ) utxo = psbt_in.non_witness_utxo.vout[txin.prevout.n] if utxo is None: continue @@ -283,7 +322,10 @@ def sign_tx(self, tx): p2wsh = True def ignore_input(): - txinputtype.address_n = [0x80000000 | 84, 0x80000000 | (1 if self.is_testnet else 0)] + txinputtype.address_n = [ + 0x80000000 | 84, + 0x80000000 | (1 if self.is_testnet else 0), + ] txinputtype.multisig = None txinputtype.script_type = proto.InputScriptType.SPENDWITNESS inputs.append(txinputtype) @@ -295,7 +337,9 @@ def ignore_input(): txinputtype.multisig = parse_multisig_xpubs(tx, psbt_in, multisig) if not is_wit: if utxo.is_p2sh: - txinputtype.script_type = proto.InputScriptType.SPENDMULTISIG + txinputtype.script_type = ( + proto.InputScriptType.SPENDMULTISIG + ) else: # Cannot sign bare multisig, ignore it ignore_input() @@ -310,16 +354,22 @@ def ignore_input(): continue # Find key to sign with - found = False # Whether we have found a key to sign with - found_in_sigs = False # Whether we have found one of our keys in the signatures + found = False # Whether we have found a key to sign with + found_in_sigs = ( + False # Whether we have found one of our keys in the signatures + ) our_keys = 0 for key in psbt_in.hd_keypaths.keys(): keypath = psbt_in.hd_keypaths[key] if keypath[0] == master_fp: - if key in psbt_in.partial_sigs: # This key already has a signature + if ( + key in psbt_in.partial_sigs + ): # This key already has a signature found_in_sigs = True continue - if not found: # This key does not have a signature and we don't have a key to sign with yet + if ( + not found + ): # This key does not have a signature and we don't have a key to sign with yet txinputtype.address_n = keypath[1:] found = True our_keys += 1 @@ -328,11 +378,15 @@ def ignore_input(): if our_keys > passes: passes = our_keys - if not found and not found_in_sigs: # None of our keys were in hd_keypaths or in partial_sigs + if ( + not found and not found_in_sigs + ): # None of our keys were in hd_keypaths or in partial_sigs # This input is not one of ours ignore_input() continue - elif not found and found_in_sigs: # All of our keys are in partial_sigs, ignore whatever signature is produced for this input + elif ( + not found and found_in_sigs + ): # All of our keys are in partial_sigs, ignore whatever signature is produced for this input ignore_input() continue @@ -341,13 +395,13 @@ def ignore_input(): # address version byte if self.is_testnet: - p2pkh_version = b'\x6f' - p2sh_version = b'\xc4' - bech32_hrp = 'tb' + p2pkh_version = b"\x6f" + p2sh_version = b"\xc4" + bech32_hrp = "tb" else: - p2pkh_version = b'\x00' - p2sh_version = b'\x05' - bech32_hrp = 'bc' + p2pkh_version = b"\x00" + p2sh_version = b"\x05" + bech32_hrp = "bc" # prepare outputs outputs = [] @@ -379,14 +433,22 @@ def ignore_input(): txoutput.address_n = keypath[1:] txoutput.address = None elif out.is_p2sh() and psbt_out.redeem_script: - wit, ver, prog = CTxOut(0, psbt_out.redeem_script).is_witness() + wit, ver, prog = CTxOut( + 0, psbt_out.redeem_script + ).is_witness() if wit and len(prog) == 20: - txoutput.script_type = proto.OutputScriptType.PAYTOP2SHWITNESS + txoutput.script_type = ( + proto.OutputScriptType.PAYTOP2SHWITNESS + ) txoutput.address_n = keypath[1:] txoutput.address = None - is_ms, multisig = parse_multisig(psbt_out.witness_script if wit else psbt_out.redeem_script) + is_ms, multisig = parse_multisig( + psbt_out.witness_script if wit else psbt_out.redeem_script + ) if is_ms: - txoutput.multisig = parse_multisig_xpubs(tx, psbt_out, multisig) + txoutput.multisig = parse_multisig_xpubs( + tx, psbt_out, multisig + ) # append to outputs outputs.append(txoutput) @@ -420,21 +482,25 @@ def ignore_input(): tx_details = proto.SignTx() tx_details.version = tx.tx.nVersion tx_details.lock_time = tx.tx.nLockTime - signed_tx = btc.sign_tx(self.client, self.coin_name, inputs, outputs, tx_details, prevtxs) + signed_tx = btc.sign_tx( + self.client, self.coin_name, inputs, outputs, tx_details, prevtxs + ) # Each input has one signature - for input_num, (psbt_in, sig) in py_enumerate(list(zip(tx.inputs, signed_tx[0]))): + for input_num, (psbt_in, sig) in py_enumerate( + list(zip(tx.inputs, signed_tx[0])) + ): if input_num in to_ignore: continue for pubkey in psbt_in.hd_keypaths.keys(): fp = psbt_in.hd_keypaths[pubkey][0] if fp == master_fp and pubkey not in psbt_in.partial_sigs: - psbt_in.partial_sigs[pubkey] = sig + b'\x01' + psbt_in.partial_sigs[pubkey] = sig + b"\x01" break p += 1 - return {'psbt': tx.serialize()} + return {"psbt": tx.serialize()} # Must return a base64 encoded string with the signed message # The message can be any string @@ -443,7 +509,7 @@ def sign_message(self, message, keypath): self._check_unlocked() path = tools.parse_path(keypath) result = btc.sign_message(self.client, self.coin_name, path, message) - return {'signature': base64.b64encode(result.signature).decode('utf-8')} + return {"signature": base64.b64encode(result.signature).decode("utf-8")} # Display address of specified type on the device. @trezor_exception @@ -455,7 +521,9 @@ def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None): # Get multisig object required by Trezor's get_address multisig = parse_multisig(bytes.fromhex(redeem_script)) if not multisig[0]: - raise BadArgumentError("The redeem script provided is not a multisig. Only multisig scripts can be displayed.") + raise BadArgumentError( + "The redeem script provided is not a multisig. Only multisig scripts can be displayed." + ) multisig = multisig[1] else: multisig = None @@ -471,11 +539,11 @@ def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None): script_type = proto.InputScriptType.SPENDADDRESS # convert device fingerprint to 'm' if exists in path - keypath = keypath.replace(self.get_master_fingerprint_hex(), 'm') + keypath = keypath.replace(self.get_master_fingerprint_hex(), "m") - for path in keypath.split(','): - if len(path.split('/')[0]) == 8: - path = path.split('/', 1)[1] + for path in keypath.split(","): + if len(path.split("/")[0]) == 8: + path = path.split("/", 1)[1] expanded_path = tools.parse_path(path) try: @@ -487,7 +555,7 @@ def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None): script_type=script_type, multisig=multisig, ) - return {'address': address} + return {"address": address} except: pass @@ -495,38 +563,48 @@ def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None): # Setup a new device @trezor_exception - def setup_device(self, label='', passphrase=''): + def setup_device(self, label="", passphrase=""): self.client.init_device() if not self.simulator: # Use interactive_get_pin self.client.ui.get_pin = MethodType(interactive_get_pin, self.client.ui) if self.client.features.initialized: - raise DeviceAlreadyInitError('Device is already initialized. Use wipe first and try again') + raise DeviceAlreadyInitError( + "Device is already initialized. Use wipe first and try again" + ) device.reset(self.client, passphrase_protection=bool(self.password)) - return {'success': True} + return {"success": True} # Wipe this device @trezor_exception def wipe_device(self): self._check_unlocked() device.wipe(self.client) - return {'success': True} + return {"success": True} # Restore device from mnemonic or xprv @trezor_exception - def restore_device(self, label='', word_count=24): + def restore_device(self, label="", word_count=24): self.client.init_device() if not self.simulator: # Use interactive_get_pin self.client.ui.get_pin = MethodType(interactive_get_pin, self.client.ui) - device.recover(self.client, word_count=word_count, label=label, input_callback=mnemonic_words(), passphrase_protection=bool(self.password)) - return {'success': True} + device.recover( + self.client, + word_count=word_count, + label=label, + input_callback=mnemonic_words(), + passphrase_protection=bool(self.password), + ) + return {"success": True} # Begin backup process - def backup_device(self, label='', passphrase=''): - raise UnavailableActionError('The {} does not support creating a backup via software'.format(self.type)) + def backup_device(self, label="", passphrase=""): + raise UnavailableActionError( + "The {} does not support creating a backup via software".format(self.type) + ) # Close the device @trezor_exception @@ -536,17 +614,30 @@ def close(self): # Prompt for a pin on device @trezor_exception def prompt_pin(self): - self.coin_name = 'Testnet' if self.is_testnet else 'Bitcoin' + self.coin_name = "Testnet" if self.is_testnet else "Bitcoin" self.client.open() self.client.init_device() if not self.client.features.pin_protection: - raise DeviceAlreadyUnlockedError('This device does not need a PIN') + raise DeviceAlreadyUnlockedError("This device does not need a PIN") if self.client.features.pin_cached: - raise DeviceAlreadyUnlockedError('The PIN has already been sent to this device') - print('Use \'sendpin\' to provide the number positions for the PIN as displayed on your device\'s screen', file=sys.stderr) + raise DeviceAlreadyUnlockedError( + "The PIN has already been sent to this device" + ) + print( + "Use 'sendpin' to provide the number positions for the PIN as displayed on your device's screen", + file=sys.stderr, + ) print(PIN_MATRIX_DESCRIPTION, file=sys.stderr) - self.client.call_raw(proto.GetPublicKey(address_n=[0x8000002c, 0x80000001, 0x80000000], ecdsa_curve_name=None, show_display=False, coin_name=self.coin_name, script_type=proto.InputScriptType.SPENDADDRESS)) - return {'success': True} + self.client.call_raw( + proto.GetPublicKey( + address_n=[0x8000002C, 0x80000001, 0x80000000], + ecdsa_curve_name=None, + show_display=False, + coin_name=self.coin_name, + script_type=proto.InputScriptType.SPENDADDRESS, + ) + ) + return {"success": True} # Send the pin @trezor_exception @@ -559,26 +650,35 @@ def send_pin(self, pin): self.client.features = self.client.call_raw(proto.GetFeatures()) if isinstance(self.client.features, proto.Features): if not self.client.features.pin_protection: - raise DeviceAlreadyUnlockedError('This device does not need a PIN') + raise DeviceAlreadyUnlockedError("This device does not need a PIN") if self.client.features.pin_cached: - raise DeviceAlreadyUnlockedError('The PIN has already been sent to this device') - return {'success': False} - return {'success': True} + raise DeviceAlreadyUnlockedError( + "The PIN has already been sent to this device" + ) + return {"success": False} + return {"success": True} # Toggle passphrase @trezor_exception def toggle_passphrase(self): self._check_unlocked() try: - device.apply_settings(self.client, use_passphrase=not self.client.features.passphrase_protection) + device.apply_settings( + self.client, + use_passphrase=not self.client.features.passphrase_protection, + ) except: - if self.type == 'Keepkey': - print('Confirm the action by entering your PIN', file=sys.stderr) - print('Use \'sendpin\' to provide the number positions for the PIN as displayed on your device\'s screen', file=sys.stderr) + if self.type == "Keepkey": + print("Confirm the action by entering your PIN", file=sys.stderr) + print( + "Use 'sendpin' to provide the number positions for the PIN as displayed on your device's screen", + file=sys.stderr, + ) print(PIN_MATRIX_DESCRIPTION, file=sys.stderr) - return {'success': True} + return {"success": True} + -def enumerate(password=''): +def enumerate(password=""): results = [] for dev in enumerate_devices(): # enumerate_devices filters to Trezors and Keepkeys. @@ -587,38 +687,51 @@ def enumerate(password=''): continue d_data = {} - d_data['type'] = 'trezor' - d_data['path'] = dev.get_path() + d_data["type"] = "trezor" + d_data["path"] = dev.get_path() client = None with handle_errors(common_err_msgs["enumerate"], d_data): - client = TrezorClient(d_data['path'], password) + client = TrezorClient(d_data["path"], password) client.client.init_device() - if 'trezor' not in client.client.features.vendor: + if "trezor" not in client.client.features.vendor: continue - d_data['model'] = 'trezor_' + client.client.features.model.lower() - if d_data['path'] == 'udp:127.0.0.1:21324': - d_data['model'] += '_simulator' - - d_data['needs_pin_sent'] = client.client.features.pin_protection and not client.client.features.pin_cached - if client.client.features.model == '1': - d_data['needs_passphrase_sent'] = client.client.features.passphrase_protection # always need the passphrase sent for Trezor One if it has passphrase protection enabled + d_data["model"] = "trezor_" + client.client.features.model.lower() + if d_data["path"] == "udp:127.0.0.1:21324": + d_data["model"] += "_simulator" + + d_data["needs_pin_sent"] = ( + client.client.features.pin_protection + and not client.client.features.pin_cached + ) + if client.client.features.model == "1": + d_data[ + "needs_passphrase_sent" + ] = ( + client.client.features.passphrase_protection + ) # always need the passphrase sent for Trezor One if it has passphrase protection enabled else: - d_data['needs_passphrase_sent'] = False - if d_data['needs_pin_sent']: - raise DeviceNotReadyError('Trezor is locked. Unlock by using \'promptpin\' and then \'sendpin\'.') - if d_data['needs_passphrase_sent'] and not password: - raise DeviceNotReadyError("Passphrase needs to be specified before the fingerprint information can be retrieved") + d_data["needs_passphrase_sent"] = False + if d_data["needs_pin_sent"]: + raise DeviceNotReadyError( + "Trezor is locked. Unlock by using 'promptpin' and then 'sendpin'." + ) + if d_data["needs_passphrase_sent"] and not password: + raise DeviceNotReadyError( + "Passphrase needs to be specified before the fingerprint information can be retrieved" + ) if client.client.features.initialized: - d_data['fingerprint'] = client.get_master_fingerprint_hex() - d_data['needs_passphrase_sent'] = False # Passphrase is always needed for the above to have worked, so it's already sent + d_data["fingerprint"] = client.get_master_fingerprint_hex() + d_data[ + "needs_passphrase_sent" + ] = False # Passphrase is always needed for the above to have worked, so it's already sent else: - d_data['error'] = 'Not initialized' - d_data['code'] = DEVICE_NOT_INITIALIZED + d_data["error"] = "Not initialized" + d_data["code"] = DEVICE_NOT_INITIALIZED if client: client.close() results.append(d_data) - return results \ No newline at end of file + return results diff --git a/src/cryptoadvance/specter/devices/hwi_device.py b/src/cryptoadvance/specter/devices/hwi_device.py index 18e88cd275..78796dac45 100644 --- a/src/cryptoadvance/specter/devices/hwi_device.py +++ b/src/cryptoadvance/specter/devices/hwi_device.py @@ -2,6 +2,7 @@ import hwilib.commands as hwi_commands import importlib + class HWIDevice(Device): hwi_support = True @@ -11,7 +12,7 @@ def __init__(self, name, alias, keys, fullpath, manager): Device.__init__(self, name, alias, keys, fullpath, manager) def create_psbts(self, base64_psbt, wallet): - return {'hwi': wallet.fill_psbt(base64_psbt)} + return {"hwi": wallet.fill_psbt(base64_psbt)} @classmethod def enumerate(cls, *args, **kwargs): diff --git a/src/cryptoadvance/specter/devices/keepkey.py b/src/cryptoadvance/specter/devices/keepkey.py index 9bbf1c48a0..9d448fd8f6 100644 --- a/src/cryptoadvance/specter/devices/keepkey.py +++ b/src/cryptoadvance/specter/devices/keepkey.py @@ -1,4 +1,5 @@ from .hwi_device import HWIDevice + # a hack that verifies multisig from .hwi import keepkey diff --git a/src/cryptoadvance/specter/devices/sd_card_device.py b/src/cryptoadvance/specter/devices/sd_card_device.py index 196511490f..abcaefc19d 100644 --- a/src/cryptoadvance/specter/devices/sd_card_device.py +++ b/src/cryptoadvance/specter/devices/sd_card_device.py @@ -11,6 +11,5 @@ def __init__(self, name, alias, keys, fullpath, manager): def create_psbts(self, base64_psbt, wallet): psbts = super().create_psbts(base64_psbt, wallet) - psbts['sdcard'] = wallet.fill_psbt( - base64_psbt, non_witness=False, xpubs=True) + psbts["sdcard"] = wallet.fill_psbt(base64_psbt, non_witness=False, xpubs=True) return psbts diff --git a/src/cryptoadvance/specter/devices/specter.py b/src/cryptoadvance/specter/devices/specter.py index c51ea7b0d2..e5b2e11cfd 100644 --- a/src/cryptoadvance/specter/devices/specter.py +++ b/src/cryptoadvance/specter/devices/specter.py @@ -3,6 +3,7 @@ from hwilib.serializations import PSBT from .hwi.specter_diy import enumerate as specter_enumerate, SpecterClient + class Specter(HWIDevice): device_type = "specter" name = "Specter-DIY" @@ -10,7 +11,7 @@ class Specter(HWIDevice): exportable_to_wallet = True sd_card_support = False qr_code_support = True - wallet_export_type = 'qr' + wallet_export_type = "qr" supports_hwi_multisig_display_address = True def __init__(self, name, alias, keys, fullpath, manager): @@ -32,9 +33,10 @@ def create_psbts(self, base64_psbt, wallet): # only contains two last derivation indexes - change and index wallet_key = b"\xfc\xca\x01" + get_wallet_fingerprint(wallet) inp.unknown[wallet_key] = b"".join( - [i.to_bytes(4, "little") for i in inp.hd_keypaths[k][-2:]]) + [i.to_bytes(4, "little") for i in inp.hd_keypaths[k][-2:]] + ) inp.hd_keypaths = {} - psbts['qrcode'] = qr_psbt.serialize() + psbts["qrcode"] = qr_psbt.serialize() return psbts def export_wallet(self, wallet): @@ -48,15 +50,16 @@ def enumerate(cls, *args, **kwargs): def get_client(cls, *args, **kwargs): return SpecterClient(*args, **kwargs) + def get_wallet_qr_descriptor(wallet): return wallet.recv_descriptor.split("#")[0].replace("/0/*", "") def get_wallet_fingerprint(wallet): """ - Unique fingerprint of the wallet - + Unique fingerprint of the wallet - first 4 bytes of hash160 of its descriptor """ h256 = hashlib.sha256(get_wallet_qr_descriptor(wallet).encode()).digest() - h160 = hashlib.new('ripemd160', h256).digest() + h160 = hashlib.new("ripemd160", h256).digest() return h160[:4] diff --git a/src/cryptoadvance/specter/devices/trezor.py b/src/cryptoadvance/specter/devices/trezor.py index d1f3e06607..af3ea9daf7 100644 --- a/src/cryptoadvance/specter/devices/trezor.py +++ b/src/cryptoadvance/specter/devices/trezor.py @@ -1,7 +1,9 @@ from .hwi_device import HWIDevice + # a hack that verifies multisig from .hwi import trezor + class Trezor(HWIDevice): device_type = "trezor" name = "Trezor" diff --git a/src/cryptoadvance/specter/helpers.py b/src/cryptoadvance/specter/helpers.py index 78dff9b4e4..c5c717c6a1 100644 --- a/src/cryptoadvance/specter/helpers.py +++ b/src/cryptoadvance/specter/helpers.py @@ -23,18 +23,22 @@ # use this for all fs operations fslock = threading.Lock() + def locked(customlock=fslock): """ @locked(lock) decorator. - Make sure you are not calling + Make sure you are not calling @locked function from another @locked function with the same lock argument. """ + def wrapper(fn): def wrapper_fn(*args, **kwargs): with customlock: return fn(*args, **kwargs) + return wrapper_fn + return wrapper @@ -44,7 +48,7 @@ def alias(name): Replaces space with _ and keeps only alphanumeric chars. """ name = name.replace(" ", "_") - return "".join(x for x in name if x.isalnum() or x=="_").lower() + return "".join(x for x in name if x.isalnum() or x == "_").lower() def deep_update(d, u): @@ -61,10 +65,9 @@ def deep_update(d, u): def load_jsons(folder, key=None): # get all json files (not hidden) - files = [f for f in os.listdir(folder) - if f.endswith(".json") and - not f.startswith(".") - ] + files = [ + f for f in os.listdir(folder) if f.endswith(".json") and not f.startswith(".") + ] files.sort(key=lambda x: os.path.getmtime(os.path.join(folder, x))) dd = OrderedDict() for fname in files: @@ -84,19 +87,19 @@ def load_jsons(folder, key=None): def which(program): - ''' mimics the "which" command in bash but even for stuff not on the path. - Also has implicit pyinstaller support - Place your executables like --add-binary '.env/bin/hwi:.' - ... and they will be found. - returns a full path of the executable and if a full path is passed, - it will simply return it if found and executable - will raise an Exception if not found - ''' + """mimics the "which" command in bash but even for stuff not on the path. + Also has implicit pyinstaller support + Place your executables like --add-binary '.env/bin/hwi:.' + ... and they will be found. + returns a full path of the executable and if a full path is passed, + it will simply return it if found and executable + will raise an Exception if not found + """ def is_exe(fpath): return os.path.isfile(fpath) and os.access(fpath, os.X_OK) - if getattr(sys, 'frozen', False): + if getattr(sys, "frozen", False): # Best understood with the snippet below this section: # https://pyinstaller.readthedocs.io/en/v3.3.1/runtime-information.html#using-sys-executable-and-sys-argv-0 exec_location = os.path.join(sys._MEIPASS, program) @@ -121,61 +124,68 @@ def is_exe(fpath): # should work in all python versions def run_shell(cmd): """ - Runs a shell command. + Runs a shell command. Example: run(["ls", "-a"]) Returns: dict({"code": returncode, "out": stdout, "err": stderr}) """ try: - proc = subprocess.Popen(cmd, - stdout = subprocess.PIPE, - stderr = subprocess.PIPE, + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, ) stdout, stderr = proc.communicate() - return { "code": proc.returncode, "out": stdout, "err": stderr } + return {"code": proc.returncode, "out": stdout, "err": stderr} except: - return { "code": 0xf00dbabe, "out": b"", "err": b"Can't run subprocess" } + return {"code": 0xF00DBABE, "out": b"", "err": b"Can't run subprocess"} def set_loglevel(app, loglevel_string): logger.info("Setting Loglevel to %s" % loglevel_string) - loglevels = { - "WARN": logging.WARN, - "INFO": logging.INFO, - "DEBUG" : logging.DEBUG - } + loglevels = {"WARN": logging.WARN, "INFO": logging.INFO, "DEBUG": logging.DEBUG} app.logger.setLevel(loglevels[loglevel_string]) logging.getLogger().setLevel(loglevels[loglevel_string]) def get_loglevel(app): - loglevels = { - logging.WARN : "WARN", - logging.INFO : "INFO", - logging.DEBUG : "DEBUG" - } + loglevels = {logging.WARN: "WARN", logging.INFO: "INFO", logging.DEBUG: "DEBUG"} return loglevels[app.logger.getEffectiveLevel()] def get_version_info(): - ''' Returns a triple of the current version (of the pip-package cryptoadvance.specter and - the latest version and whether you should upgrade - ''' - name="cryptoadvance.specter" + """Returns a triple of the current version (of the pip-package cryptoadvance.specter and + the latest version and whether you should upgrade + """ + name = "cryptoadvance.specter" try: # fail right away if it's a binary - if getattr(sys, 'frozen', False): + if getattr(sys, "frozen", False): raise RuntimeError("Using frozen binary, verision unavailable") - latest_version = str(subprocess.run([sys.executable, '-m', 'pip', 'install', '{}==random'.format(name)], capture_output=True, text=True)) - latest_version = latest_version[latest_version.find('(from versions:')+15:] - latest_version = latest_version[:latest_version.find(')')] - latest_version = latest_version.replace(' ','').split(',')[-1] - - current_version = str(subprocess.run([sys.executable, '-m', 'pip', 'show', '{}'.format(name)], capture_output=True, text=True)) - current_version = current_version[current_version.find('Version:')+8:] - current_version = current_version[:current_version.find('\\n')].replace(' ','') + latest_version = str( + subprocess.run( + [sys.executable, "-m", "pip", "install", "{}==random".format(name)], + capture_output=True, + text=True, + ) + ) + latest_version = latest_version[latest_version.find("(from versions:") + 15 :] + latest_version = latest_version[: latest_version.find(")")] + latest_version = latest_version.replace(" ", "").split(",")[-1] + + current_version = str( + subprocess.run( + [sys.executable, "-m", "pip", "show", "{}".format(name)], + capture_output=True, + text=True, + ) + ) + current_version = current_version[current_version.find("Version:") + 8 :] + current_version = current_version[: current_version.find("\\n")].replace( + " ", "" + ) # master? - if current_version == 'vx.y.z-get-replaced-by-release-script': - current_version = 'custom' + if current_version == "vx.y.z-get-replaced-by-release-script": + current_version = "custom" if re.search(r"v?([\d+]).([\d+]).([\d+]).*", current_version): return current_version, latest_version, latest_version != current_version @@ -188,13 +198,13 @@ def get_version_info(): def hwi_get_config(specter): - config = { - 'whitelisted_domains': 'http://127.0.0.1:25441/' - } + config = {"whitelisted_domains": "http://127.0.0.1:25441/"} # if hwi_bridge_config.json file exists - load from it if os.path.isfile(os.path.join(specter.data_folder, "hwi_bridge_config.json")): with fslock: - with open(os.path.join(specter.data_folder, "hwi_bridge_config.json"), "r") as f: + with open( + os.path.join(specter.data_folder, "hwi_bridge_config.json"), "r" + ) as f: file_config = json.load(f) deep_update(config, file_config) # otherwise - create one and assign unique id @@ -204,35 +214,37 @@ def hwi_get_config(specter): def save_hwi_bridge_config(specter, config): - if 'whitelisted_domains' in config: - whitelisted_domains = '' - for url in config['whitelisted_domains'].split(): - if not url.endswith("/") and url != '*': + if "whitelisted_domains" in config: + whitelisted_domains = "" + for url in config["whitelisted_domains"].split(): + if not url.endswith("/") and url != "*": # make sure the url end with a "/" url += "/" - whitelisted_domains += url.strip() + '\n' - config['whitelisted_domains'] = whitelisted_domains + whitelisted_domains += url.strip() + "\n" + config["whitelisted_domains"] = whitelisted_domains with fslock: - with open(os.path.join(specter.data_folder, 'hwi_bridge_config.json'), "w") as f: + with open( + os.path.join(specter.data_folder, "hwi_bridge_config.json"), "w" + ) as f: json.dump(config, f, indent=4) def der_to_bytes(derivation): items = derivation.split("/") if len(items) == 0: - return b'' - if items[0] == 'm': + return b"" + if items[0] == "m": items = items[1:] - if len(items) > 0 and items[-1] == '': + if len(items) > 0 and items[-1] == "": items = items[:-1] - res = b'' + res = b"" for item in items: index = 0 - if item[-1] == 'h' or item[-1] == "'": + if item[-1] == "h" or item[-1] == "'": index += 0x80000000 item = item[:-1] index += int(item) - res += index.to_bytes(4,'little') + res += index.to_bytes(4, "little") return res @@ -243,8 +255,12 @@ def get_devices_with_keys_by_type(app, cosigners, wallet_type): prefix = "xpub" for cosigner in cosigners: device = copy.deepcopy(cosigner) - allowed_types = ['', wallet_type] - device.keys = [key for key in device.keys if key.xpub.startswith(prefix) and key.key_type in allowed_types] + allowed_types = ["", wallet_type] + device.keys = [ + key + for key in device.keys + if key.xpub.startswith(prefix) and key.key_type in allowed_types + ] devices.append(device) return devices @@ -260,12 +276,15 @@ def sort_descriptor(rpc, descriptor, index=None, change=False): # get pubkeys involved address_info = rpc.getaddressinfo(address) - if 'pubkeys' in address_info: + if "pubkeys" in address_info: pubkeys = address_info["pubkeys"] - elif 'embedded' in address_info and 'pubkeys' in address_info['embedded']: + elif "embedded" in address_info and "pubkeys" in address_info["embedded"]: pubkeys = address_info["embedded"]["pubkeys"] else: - raise Exception("Could not find 'pubkeys' in address info:\n%s" % json.dumps(address_info, indent=2)) + raise Exception( + "Could not find 'pubkeys' in address info:\n%s" + % json.dumps(address_info, indent=2) + ) # get xpubs from the descriptor arr = descriptor.split("(multi(")[1].split(")")[0].split(",") @@ -276,9 +295,9 @@ def sort_descriptor(rpc, descriptor, index=None, change=False): keys = arr[1:] # sort them according to sortedmulti - z = sorted(zip(pubkeys,keys), key=lambda x: x[0]) + z = sorted(zip(pubkeys, keys), key=lambda x: x[0]) keys = [zz[1] for zz in z] - inner = f"{sigs_required},"+",".join(keys) + inner = f"{sigs_required}," + ",".join(keys) desc = f"multi({inner})" # Write from the inside out @@ -314,16 +333,16 @@ def get_txid(tx): def get_startblock_by_chain(specter): - if specter.info['chain'] == "main": - if not specter.info['pruned'] or specter.info['pruneheight'] < 481824: + if specter.info["chain"] == "main": + if not specter.info["pruned"] or specter.info["pruneheight"] < 481824: startblock = 481824 else: - startblock = specter.info['pruneheight'] + startblock = specter.info["pruneheight"] else: - if not specter.info['pruned']: + if not specter.info["pruned"]: startblock = 0 else: - startblock = specter.info['pruneheight'] + startblock = specter.info["pruneheight"] return startblock @@ -338,17 +357,17 @@ def generate_mnemonic(strength=256): # Transaction processing helpers def parse_utxo(wallet, utxo): for tx in utxo: - tx_data = wallet.rpc.gettransaction(tx['txid']) - tx['time'] = tx_data['time'] - if (len(tx_data['details']) > 1): - for details in tx_data['details']: - if details['category'] != 'send': - tx['category'] = details['category'] + tx_data = wallet.rpc.gettransaction(tx["txid"]) + tx["time"] = tx_data["time"] + if len(tx_data["details"]) > 1: + for details in tx_data["details"]: + if details["category"] != "send": + tx["category"] = details["category"] break - else: - tx['category'] = tx_data['details'][0]['category'] - if 'confirmations' in tx_data: - tx['confirmations'] = tx_data['confirmations'] else: - tx['confirmations'] = 0 + tx["category"] = tx_data["details"][0]["category"] + if "confirmations" in tx_data: + tx["confirmations"] = tx_data["confirmations"] + else: + tx["confirmations"] = 0 return utxo diff --git a/src/cryptoadvance/specter/hwi_rpc.py b/src/cryptoadvance/specter/hwi_rpc.py index 1917c549b4..1c151b1b7e 100644 --- a/src/cryptoadvance/specter/hwi_rpc.py +++ b/src/cryptoadvance/specter/hwi_rpc.py @@ -11,22 +11,25 @@ logger = logging.getLogger(__name__) -hwi_classes = [ cls for cls in device_classes if cls.hwi_support ] +hwi_classes = [cls for cls in device_classes if cls.hwi_support] # use this lock for all hwi operations hwilock = threading.Lock() + def get_device_class(device_type): for cls in hwi_classes: if cls.device_type == device_type: return cls + class HWIBridge(JSONRPC): """ A class that represents HWI JSON-RPC methods. All methods of this class are callable over JSON-RPC, except _underscored. """ + def __init__(self): self.exposed_rpc = { "enumerate": self.enumerate, @@ -37,16 +40,16 @@ def __init__(self): "extract_xpubs": self.extract_xpubs, "display_address": self.display_address, "sign_tx": self.sign_tx, - "sign_message": self.sign_message + "sign_message": self.sign_message, } # Running enumerate after beginning an interaction with a specific device # crashes python or make HWI misbehave. For now we just get all connected # devices once per session and save them. - print("Initializing HWI...") # to explain user why it takes so long + print("Initializing HWI...") # to explain user why it takes so long self.enumerate() @locked(hwilock) - def enumerate(self, passphrase='', chain=''): + def enumerate(self, passphrase="", chain=""): """ Returns a list of all connected devices (dicts). Standard HWI enumerate() command + Specter. @@ -67,14 +70,15 @@ def enumerate(self, passphrase='', chain=''): if "needs_pin_sent" in dev and dev["needs_pin_sent"]: continue # we can't get fingerprint if passphrase is not provided - if ("needs_passphrase_sent" in dev + if ( + "needs_passphrase_sent" in dev and dev["needs_passphrase_sent"] and passphrase is None ): continue client = devcls.get_client(dev["path"], passphrase) try: - dev['fingerprint'] = client.get_master_fingerprint_hex() + dev["fingerprint"] = client.get_master_fingerprint_hex() finally: client.close() devices += devs @@ -82,9 +86,11 @@ def enumerate(self, passphrase='', chain=''): self.devices = devices return self.devices - def detect_device(self, device_type=None, path=None, fingerprint=None, rescan_devices=False): + def detect_device( + self, device_type=None, path=None, fingerprint=None, rescan_devices=False + ): """ - Returns a hardware wallet details + Returns a hardware wallet details with specific fingerprint/ path/ type or None if not connected. If found multiple devices return only one. @@ -94,27 +100,37 @@ def detect_device(self, device_type=None, path=None, fingerprint=None, rescan_de res = [] if device_type is not None: - res = [dev for dev in self.devices if dev["type"].lower() == device_type.lower()] + res = [ + dev + for dev in self.devices + if dev["type"].lower() == device_type.lower() + ] if fingerprint is not None: - res = [dev for dev in self.devices if dev["fingerprint"].lower() == fingerprint.lower()] + res = [ + dev + for dev in self.devices + if dev["fingerprint"].lower() == fingerprint.lower() + ] if path is not None: res = [dev for dev in self.devices if dev["path"] == path] if len(res) > 0: return res[0] @locked(hwilock) - def toggle_passphrase(self, device_type=None, path=None, passphrase='', chain=''): + def toggle_passphrase(self, device_type=None, path=None, passphrase="", chain=""): if device_type == "keepkey" or device_type == "trezor": - with self._get_client(device_type=device_type, - path=path, - passphrase=passphrase, - chain=chain) as client: + with self._get_client( + device_type=device_type, path=path, passphrase=passphrase, chain=chain + ) as client: return hwi_commands.toggle_passphrase(client) else: - raise Exception("Invalid HWI device type %s, toggle_passphrase is only supported for Trezor and Keepkey devices" % device_type) + raise Exception( + "Invalid HWI device type %s, toggle_passphrase is only supported for Trezor and Keepkey devices" + % device_type + ) @locked(hwilock) - def prompt_pin(self, device_type=None, path=None, passphrase='', chain=''): + def prompt_pin(self, device_type=None, path=None, passphrase="", chain=""): if device_type == "keepkey" or device_type == "trezor": # The device will randomize its pin entry matrix on the device # but the corresponding digits in the receiving UI always map @@ -122,117 +138,170 @@ def prompt_pin(self, device_type=None, path=None, passphrase='', chain=''): # 7 8 9 # 4 5 6 # 1 2 3 - with self._get_client(device_type=device_type, - path=path, - passphrase=passphrase, - chain=chain) as client: + with self._get_client( + device_type=device_type, path=path, passphrase=passphrase, chain=chain + ) as client: return hwi_commands.prompt_pin(client) else: - raise Exception("Invalid HWI device type %s, prompt_pin is only supported for Trezor and Keepkey devices" % device_type) + raise Exception( + "Invalid HWI device type %s, prompt_pin is only supported for Trezor and Keepkey devices" + % device_type + ) @locked(hwilock) - def send_pin(self, pin='', device_type=None, path=None, passphrase='', chain=''): + def send_pin(self, pin="", device_type=None, path=None, passphrase="", chain=""): if device_type == "keepkey" or device_type == "trezor": - if pin == '': + if pin == "": raise Exception("Must enter a non-empty PIN") - with self._get_client(device_type=device_type, - path=path, - passphrase=passphrase, - chain=chain) as client: + with self._get_client( + device_type=device_type, path=path, passphrase=passphrase, chain=chain + ) as client: return hwi_commands.send_pin(client, pin) else: - raise Exception("Invalid HWI device type %s, send_pin is only supported for Trezor and Keepkey devices" % device_type) + raise Exception( + "Invalid HWI device type %s, send_pin is only supported for Trezor and Keepkey devices" + % device_type + ) @locked(hwilock) - def extract_xpubs(self, account=0, device_type=None, path=None, fingerprint=None, passphrase='', chain=''): - with self._get_client(device_type=device_type, - fingerprint=fingerprint, - path=path, - passphrase=passphrase, - chain=chain) as client: + def extract_xpubs( + self, + account=0, + device_type=None, + path=None, + fingerprint=None, + passphrase="", + chain="", + ): + with self._get_client( + device_type=device_type, + fingerprint=fingerprint, + path=path, + passphrase=passphrase, + chain=chain, + ) as client: xpubs = self._extract_xpubs_from_client(client, account) return xpubs @locked(hwilock) - def display_address(self, descriptor='', device_type=None, path=None, fingerprint=None, passphrase='', chain=''): - if descriptor == '': + def display_address( + self, + descriptor="", + device_type=None, + path=None, + fingerprint=None, + passphrase="", + chain="", + ): + if descriptor == "": raise Exception("Descriptor must not be empty") - with self._get_client(device_type=device_type, - fingerprint=fingerprint, - path=path, - passphrase=passphrase, - chain=chain) as client: + with self._get_client( + device_type=device_type, + fingerprint=fingerprint, + path=path, + passphrase=passphrase, + chain=chain, + ) as client: status = hwi_commands.displayaddress(client, desc=descriptor) client.close() - if 'error' in status: - raise Exception(status['error']) - elif 'address' in status: - return status['address'] + if "error" in status: + raise Exception(status["error"]) + elif "address" in status: + return status["address"] else: raise Exception("Failed to validate address on device: Unknown Error") @locked(hwilock) - def sign_tx(self, psbt='', device_type=None, path=None, fingerprint=None, passphrase='', chain=''): - if psbt == '': + def sign_tx( + self, + psbt="", + device_type=None, + path=None, + fingerprint=None, + passphrase="", + chain="", + ): + if psbt == "": raise Exception("PSBT must not be empty") - with self._get_client(device_type=device_type, - fingerprint=fingerprint, - path=path, - passphrase=passphrase, - chain=chain) as client: + with self._get_client( + device_type=device_type, + fingerprint=fingerprint, + path=path, + passphrase=passphrase, + chain=chain, + ) as client: status = hwi_commands.signtx(client, psbt) - if 'error' in status: - raise Exception(status['error']) - elif 'psbt' in status: - return status['psbt'] + if "error" in status: + raise Exception(status["error"]) + elif "psbt" in status: + return status["psbt"] else: raise Exception("Failed to sign transaction with device: Unknown Error") @locked(hwilock) - def sign_message(self, message='', derivation_path='m', device_type=None, path=None, fingerprint=None, passphrase='', chain=''): - if message == '': + def sign_message( + self, + message="", + derivation_path="m", + device_type=None, + path=None, + fingerprint=None, + passphrase="", + chain="", + ): + if message == "": raise Exception("Message must not be empty") print(derivation_path) - with self._get_client(device_type=device_type, - fingerprint=fingerprint, - path=path, - passphrase=passphrase, - chain=chain) as client: + with self._get_client( + device_type=device_type, + fingerprint=fingerprint, + path=path, + passphrase=passphrase, + chain=chain, + ) as client: status = hwi_commands.signmessage(client, message, derivation_path) - if 'error' in status: - raise Exception(status['error']) - elif 'signature' in status: - return status['signature'] + if "error" in status: + raise Exception(status["error"]) + elif "signature" in status: + return status["signature"] else: raise Exception("Failed to sign message with device: Unknown Error") ######################## HWI Utils ######################## @contextmanager - def _get_client(self, device_type=None, path=None, fingerprint=None, passphrase='', chain=''): - """ - Returns a hardware wallet class instance - with specific fingerprint or/and path - or raises a not found error if not connected. - If found multiple devices return only one. - """ - # We do not use fingerprint in most cases since if the device is a trezor - # or a keepkey and passphrase is enabled but empty (an empty string like '') - # The device will not return the fingerprint properly. - device = self.detect_device(device_type=device_type, fingerprint=fingerprint, path=path) - if device: - devcls = get_device_class(device["type"]) - if devcls: - client = devcls.get_client(device["path"], passphrase) - if not client: - raise Exception('The device was identified but could not be reached. Please check it is properly connected and try again') - try: - client.is_testnet = chain != 'main' - yield client - finally: - client.close() - else: - raise Exception('The device could not be found. Please check it is properly connected and try again') + def _get_client( + self, device_type=None, path=None, fingerprint=None, passphrase="", chain="" + ): + """ + Returns a hardware wallet class instance + with specific fingerprint or/and path + or raises a not found error if not connected. + If found multiple devices return only one. + """ + # We do not use fingerprint in most cases since if the device is a trezor + # or a keepkey and passphrase is enabled but empty (an empty string like '') + # The device will not return the fingerprint properly. + device = self.detect_device( + device_type=device_type, fingerprint=fingerprint, path=path + ) + if device: + devcls = get_device_class(device["type"]) + if devcls: + client = devcls.get_client(device["path"], passphrase) + if not client: + raise Exception( + "The device was identified but could not be reached. Please check it is properly connected and try again" + ) + try: + client.is_testnet = chain != "main" + yield client + finally: + client.close() + else: + raise Exception( + "The device could not be found. Please check it is properly connected and try again" + ) def _extract_xpubs_from_client(self, client, account=0): try: @@ -251,102 +320,78 @@ def _extract_xpubs_from_client(self, client, account=0): # Extract nested Segwit try: - xpub = client.get_pubkey_at_path( - 'm/49h/0h/{}h'.format(account) - )['xpub'] - ypub = convert_xpub_prefix(xpub, b'\x04\x9d\x7c\xb2') + xpub = client.get_pubkey_at_path("m/49h/0h/{}h".format(account))["xpub"] + ypub = convert_xpub_prefix(xpub, b"\x04\x9d\x7c\xb2") xpubs += "[{}/49'/0'/{}']{}\n".format(master_fpr, account, ypub) except Exception: - logger.warn( - "Failed to import Nested Segwit singlesig mainnet key." - ) + logger.warn("Failed to import Nested Segwit singlesig mainnet key.") try: # native Segwit - xpub = client.get_pubkey_at_path( - 'm/84h/0h/{}h'.format(account) - )['xpub'] - zpub = convert_xpub_prefix(xpub, b'\x04\xb2\x47\x46') + xpub = client.get_pubkey_at_path("m/84h/0h/{}h".format(account))["xpub"] + zpub = convert_xpub_prefix(xpub, b"\x04\xb2\x47\x46") xpubs += "[{}/84'/0'/{}']{}\n".format(master_fpr, account, zpub) except Exception: - logger.warn( - "Failed to import native Segwit singlesig mainnet key." - ) + logger.warn("Failed to import native Segwit singlesig mainnet key.") try: # Multisig nested Segwit - xpub = client.get_pubkey_at_path( - 'm/48h/0h/{}h/1h'.format(account) - )['xpub'] - Ypub = convert_xpub_prefix(xpub, b'\x02\x95\xb4\x3f') + xpub = client.get_pubkey_at_path("m/48h/0h/{}h/1h".format(account))[ + "xpub" + ] + Ypub = convert_xpub_prefix(xpub, b"\x02\x95\xb4\x3f") xpubs += "[{}/48'/0'/{}'/1']{}\n".format(master_fpr, account, Ypub) except Exception: - logger.warn( - "Failed to import Nested Segwit multisig mainnet key." - ) + logger.warn("Failed to import Nested Segwit multisig mainnet key.") try: # Multisig native Segwit - xpub = client.get_pubkey_at_path( - 'm/48h/0h/{}h/2h'.format(account) - )['xpub'] - Zpub = convert_xpub_prefix(xpub, b'\x02\xaa\x7e\xd3') + xpub = client.get_pubkey_at_path("m/48h/0h/{}h/2h".format(account))[ + "xpub" + ] + Zpub = convert_xpub_prefix(xpub, b"\x02\xaa\x7e\xd3") xpubs += "[{}/48'/0'/{}'/2']{}\n".format(master_fpr, account, Zpub) except Exception: - logger.warn( - "Failed to import native Segwit multisig mainnet key." - ) + logger.warn("Failed to import native Segwit multisig mainnet key.") # And testnet client.is_testnet = True try: # Testnet nested Segwit - xpub = client.get_pubkey_at_path( - 'm/49h/1h/{}h'.format(account) - )['xpub'] - upub = convert_xpub_prefix(xpub, b'\x04\x4a\x52\x62') + xpub = client.get_pubkey_at_path("m/49h/1h/{}h".format(account))["xpub"] + upub = convert_xpub_prefix(xpub, b"\x04\x4a\x52\x62") xpubs += "[{}/49'/1'/{}']{}\n".format(master_fpr, account, upub) except Exception: - logger.warn( - "Failed to import Nested Segwit singlesig testnet key." - ) + logger.warn("Failed to import Nested Segwit singlesig testnet key.") try: # Testnet native Segwit - xpub = client.get_pubkey_at_path( - 'm/84h/1h/{}h'.format(account) - )['xpub'] - vpub = convert_xpub_prefix(xpub, b'\x04\x5f\x1c\xf6') + xpub = client.get_pubkey_at_path("m/84h/1h/{}h".format(account))["xpub"] + vpub = convert_xpub_prefix(xpub, b"\x04\x5f\x1c\xf6") xpubs += "[{}/84'/1'/{}']{}\n".format(master_fpr, account, vpub) except Exception: - logger.warn( - "Failed to import native Segwit singlesig testnet key." - ) + logger.warn("Failed to import native Segwit singlesig testnet key.") try: # Testnet multisig nested Segwit - xpub = client.get_pubkey_at_path( - 'm/48h/1h/{}h/1h'.format(account) - )['xpub'] - Upub = convert_xpub_prefix(xpub, b'\x02\x42\x89\xef') + xpub = client.get_pubkey_at_path("m/48h/1h/{}h/1h".format(account))[ + "xpub" + ] + Upub = convert_xpub_prefix(xpub, b"\x02\x42\x89\xef") xpubs += "[{}/48'/1'/{}'/1']{}\n".format(master_fpr, account, Upub) except Exception: - logger.warn( - "Failed to import Nested Segwit multisigsig testnet key." - ) + logger.warn("Failed to import Nested Segwit multisigsig testnet key.") try: # Testnet multisig native Segwit - xpub = client.get_pubkey_at_path( - 'm/48h/1h/{}h/2h'.format(account) - )['xpub'] - Vpub = convert_xpub_prefix(xpub, b'\x02\x57\x54\x83') + xpub = client.get_pubkey_at_path("m/48h/1h/{}h/2h".format(account))[ + "xpub" + ] + Vpub = convert_xpub_prefix(xpub, b"\x02\x57\x54\x83") xpubs += "[{}/48'/1'/{}'/2']{}\n".format(master_fpr, account, Vpub) except Exception: - logger.warn( - "Failed to import native Segwit multisig testnet key." - ) + logger.warn("Failed to import native Segwit multisig testnet key.") # Do proper cleanup otherwise have to reconnect device to access again client.close() diff --git a/src/cryptoadvance/specter/hwi_server.py b/src/cryptoadvance/specter/hwi_server.py index 66817c6341..2e4e4cac2b 100644 --- a/src/cryptoadvance/specter/hwi_server.py +++ b/src/cryptoadvance/specter/hwi_server.py @@ -6,70 +6,111 @@ from .helpers import deep_update, hwi_get_config, save_hwi_bridge_config -hwi_server = Blueprint('hwi_server', __name__) +hwi_server = Blueprint("hwi_server", __name__) CORS(hwi_server) -rand = random.randint(0, 1e32) # to force style refresh +rand = random.randint(0, 1e32) # to force style refresh hwi = HWIBridge() + @hwi_server.route("/", methods=["GET"]) def index(): - return redirect('/hwi/settings') + return redirect("/hwi/settings") + @hwi_server.route("/api/", methods=["POST"]) def api(): """JSON-RPC for HWI Bridge""" # if cross-origin - if 'HTTP_HOST' in request.environ and 'HTTP_ORIGIN' in request.environ and request.environ['HTTP_HOST'] != request.environ['HTTP_ORIGIN'].split("://")[1]: - whitelisted_domains = hwi_get_config(app.specter)['whitelisted_domains'].split() + if ( + "HTTP_HOST" in request.environ + and "HTTP_ORIGIN" in request.environ + and request.environ["HTTP_HOST"] + != request.environ["HTTP_ORIGIN"].split("://")[1] + ): + whitelisted_domains = hwi_get_config(app.specter)["whitelisted_domains"].split() for i, url in enumerate(whitelisted_domains): # might be https as well - whitelisted_domains[i] = url.replace('://localhost:', '://127.0.0.1:') - if '*' not in whitelisted_domains: - origin_url = request.environ['HTTP_ORIGIN'].replace('://localhost:', '://127.0.0.1:') + whitelisted_domains[i] = url.replace("://localhost:", "://127.0.0.1:") + if "*" not in whitelisted_domains: + origin_url = request.environ["HTTP_ORIGIN"].replace( + "://localhost:", "://127.0.0.1:" + ) if not origin_url.endswith("/"): - # make sure the url end with a "/" - origin_url += "/" - if not(origin_url in whitelisted_domains): - return jsonify({ - "jsonrpc": "2.0", - "error": { "code": -32001, "message": "Unauthorized request origin.
You must first whitelist this website URL in HWIBridge settings to grant it access." }, - "id": None - }), 500 + # make sure the url end with a "/" + origin_url += "/" + if not (origin_url in whitelisted_domains): + return ( + jsonify( + { + "jsonrpc": "2.0", + "error": { + "code": -32001, + "message": "Unauthorized request origin.
You must first whitelist this website URL in HWIBridge settings to grant it access.", + }, + "id": None, + } + ), + 500, + ) try: data = json.loads(request.data) except: - return jsonify({ - "jsonrpc": "2.0", - "error": { "code": -32700, "message": "Parse error" }, - "id": None - }), 500 - if ('forwarded_request' not in data or not data['forwarded_request']) and (app.specter.hwi_bridge_url.startswith('http://') or app.specter.hwi_bridge_url.startswith('https://')): - if ('HTTP_ORIGIN' not in request.environ): - return jsonify({ + return ( + jsonify( + { "jsonrpc": "2.0", - "error": { "code": -32600, "message": "Request must specify its origin or set `forwarded_request` to `true`." }, - "id": None - }), 500 - data['forwarded_request'] = True - requests_session = requests.Session() - requests_session.headers.update({'origin': request.environ['HTTP_ORIGIN']}) - if '.onion/' in app.specter.hwi_bridge_url: - requests_session.proxies = {} - requests_session.proxies['http'] = 'socks5h://localhost:9050' - requests_session.proxies['https'] = 'socks5h://localhost:9050' - forwarded_request = requests_session.post(app.specter.hwi_bridge_url, data=json.dumps(data)) - response = json.loads(forwarded_request.content) - return jsonify(response) + "error": {"code": -32700, "message": "Parse error"}, + "id": None, + } + ), + 500, + ) + if ("forwarded_request" not in data or not data["forwarded_request"]) and ( + app.specter.hwi_bridge_url.startswith("http://") + or app.specter.hwi_bridge_url.startswith("https://") + ): + if "HTTP_ORIGIN" not in request.environ: + return ( + jsonify( + { + "jsonrpc": "2.0", + "error": { + "code": -32600, + "message": "Request must specify its origin or set `forwarded_request` to `true`.", + }, + "id": None, + } + ), + 500, + ) + data["forwarded_request"] = True + requests_session = requests.Session() + requests_session.headers.update({"origin": request.environ["HTTP_ORIGIN"]}) + if ".onion/" in app.specter.hwi_bridge_url: + requests_session.proxies = {} + requests_session.proxies["http"] = "socks5h://localhost:9050" + requests_session.proxies["https"] = "socks5h://localhost:9050" + forwarded_request = requests_session.post( + app.specter.hwi_bridge_url, data=json.dumps(data) + ) + response = json.loads(forwarded_request.content) + return jsonify(response) return jsonify(hwi.jsonrpc(data)) -@hwi_server.route('/settings/', methods=["GET", "POST"]) + +@hwi_server.route("/settings/", methods=["GET", "POST"]) def hwi_bridge_settings(): config = hwi_get_config(app.specter) - if request.method == 'POST': - action = request.form['action'] + if request.method == "POST": + action = request.form["action"] if action == "update": - config['whitelisted_domains'] = request.form['whitelisted_domains'] + config["whitelisted_domains"] = request.form["whitelisted_domains"] save_hwi_bridge_config(app.specter, config) flash("Whitelist is updated!") - return render_template("hwi_bridge.jinja", specter=app.specter, whitelisted_domains=config['whitelisted_domains'], rand=rand) + return render_template( + "hwi_bridge.jinja", + specter=app.specter, + whitelisted_domains=config["whitelisted_domains"], + rand=rand, + ) diff --git a/src/cryptoadvance/specter/key.py b/src/cryptoadvance/specter/key.py index 55b021e41f..aa41150640 100644 --- a/src/cryptoadvance/specter/key.py +++ b/src/cryptoadvance/specter/key.py @@ -4,43 +4,46 @@ from .util.xpub import get_xpub_fingerprint -purposes = OrderedDict({ - '': "General", - "wpkh": "Single (Segwit)", - "sh-wpkh": "Single (Nested)", - "pkh": "Single (Legacy)", - "wsh": "Multisig (Segwit)", - "sh-wsh": "Multisig (Nested)", - "sh": "Multisig (Legacy)", -}) +purposes = OrderedDict( + { + "": "General", + "wpkh": "Single (Segwit)", + "sh-wpkh": "Single (Nested)", + "pkh": "Single (Legacy)", + "wsh": "Multisig (Segwit)", + "sh-wsh": "Multisig (Nested)", + "sh": "Multisig (Legacy)", + } +) VALID_PREFIXES = { - b"\x04\x35\x87\xcf": { # testnet - b"\x04\x35\x87\xcf": '', # unknown, maybe pkh + b"\x04\x35\x87\xcf": { # testnet + b"\x04\x35\x87\xcf": "", # unknown, maybe pkh b"\x04\x4a\x52\x62": "sh-wpkh", b"\x04\x5f\x1c\xf6": "wpkh", b"\x02\x42\x89\xef": "sh-wsh", b"\x02\x57\x54\x83": "wsh", }, - b"\x04\x88\xb2\x1e": { # mainnet - b"\x04\x88\xb2\x1e": '', # unknown, maybe pkh + b"\x04\x88\xb2\x1e": { # mainnet + b"\x04\x88\xb2\x1e": "", # unknown, maybe pkh b"\x04\x9d\x7c\xb2": "sh-wpkh", b"\x04\xb2\x47\x46": "wpkh", b"\x02\x95\xb4\x3f": "sh-wsh", b"\x02\xaa\x7e\xd3": "wsh", - } + }, } + class Key: def __init__(self, original, fingerprint, derivation, key_type, xpub): if key_type is None: key_type = "" - if fingerprint is None or fingerprint == '': + if fingerprint is None or fingerprint == "": fingerprint = get_xpub_fingerprint(original).hex() if derivation is None: - derivation = '' + derivation = "" if key_type not in purposes: - raise Exception('Invalid key type specified: {}.') + raise Exception("Invalid key type specified: {}.") self.original = original self.fingerprint = fingerprint self.derivation = derivation @@ -49,24 +52,24 @@ def __init__(self, original, fingerprint, derivation, key_type, xpub): @classmethod def from_json(cls, key_dict): - original = key_dict['original'] if 'original' in key_dict else '' - fingerprint = key_dict['fingerprint'] if 'fingerprint' in key_dict else '' - derivation = key_dict['derivation'] if 'derivation' in key_dict else '' - key_type = key_dict['type'] if 'type' in key_dict else '' - xpub = key_dict['xpub'] if 'xpub' in key_dict else '' + original = key_dict["original"] if "original" in key_dict else "" + fingerprint = key_dict["fingerprint"] if "fingerprint" in key_dict else "" + derivation = key_dict["derivation"] if "derivation" in key_dict else "" + key_type = key_dict["type"] if "type" in key_dict else "" + xpub = key_dict["xpub"] if "xpub" in key_dict else "" return cls(original, fingerprint, derivation, key_type, xpub) @classmethod def parse_xpub(cls, xpub): - derivation = '' + derivation = "" arr = xpub.strip().split("]") original = arr[-1] if len(arr) > 1: - derivation = arr[0].replace("'","h").lower() + derivation = arr[0].replace("'", "h").lower() xpub = arr[1] - fingerprint = '' - if derivation != '': + fingerprint = "" + if derivation != "": if derivation[0] != "[": raise Exception("Missing leading [") derivation_path = derivation[1:].split("/") @@ -90,13 +93,13 @@ def parse_xpub(cls, xpub): derivation_path[0] = "m" derivation = "/".join(derivation_path) else: - derivation = '' + derivation = "" # checking xpub prefix and defining key type xpub_bytes = decode_base58(xpub, num_bytes=82) prefix = xpub_bytes[:4] is_valid = False - key_type = '' + key_type = "" for k in VALID_PREFIXES: if prefix in VALID_PREFIXES[k].keys(): key_type = VALID_PREFIXES[k][prefix] @@ -110,7 +113,7 @@ def parse_xpub(cls, xpub): xpub = encode_base58_checksum(xpub_bytes) # defining key type from derivation - if derivation != '' and key_type == '': + if derivation != "" and key_type == "": derivation_path = derivation.split("/") purpose = derivation_path[1] if purpose == "44h": @@ -138,7 +141,7 @@ def parse_xpub(cls, xpub): fingerprint = hexlify(xpub_bytes[5:9]).decode() index = int.from_bytes(xpub_bytes[9:13], "big") is_hardened = bool(index & 0x8000_0000) - derivation = "m/%d%s" % (index & 0x7fff_ffff, "h" if is_hardened else "") + derivation = "m/%d%s" % (index & 0x7FFF_FFFF, "h" if is_hardened else "") return cls(original, fingerprint, derivation, key_type, xpub) @@ -155,14 +158,17 @@ def parse_xpubs(cls, xpubs): failed.append(line + "\n" + str(e)) return keys, failed - @property def metadata(self): metadata = {} metadata["chain"] = "Mainnet" if self.xpub.startswith("xpub") else "Testnet" metadata["purpose"] = self.purpose if self.derivation is not None: - metadata["combined"] = "[%s%s]%s" % (self.fingerprint, self.derivation[1:], self.xpub) + metadata["combined"] = "[%s%s]%s" % ( + self.fingerprint, + self.derivation[1:], + self.xpub, + ) else: metadata["combined"] = self.xpub return metadata @@ -174,11 +180,11 @@ def is_testnet(self): @property def json(self): return { - 'original': self.original, - 'fingerprint': self.fingerprint, - 'derivation': self.derivation, - 'type': self.key_type, - 'xpub': self.xpub + "original": self.original, + "fingerprint": self.fingerprint, + "derivation": self.derivation, + "type": self.key_type, + "xpub": self.xpub, } @property @@ -187,8 +193,7 @@ def purpose(self): def to_string(self, slip132=True): if self.derivation and self.fingerprint: - path_str = \ - f"/{self.derivation[2:]}" if self.derivation != "m" else "" + path_str = f"/{self.derivation[2:]}" if self.derivation != "m" else "" return f"[{self.fingerprint}{path_str}]{self.original if slip132 else self.xpub}" else: return self.original if slip132 else self.xpub diff --git a/src/cryptoadvance/specter/rpc.py b/src/cryptoadvance/specter/rpc.py index fa6c562301..cf1917d31d 100644 --- a/src/cryptoadvance/specter/rpc.py +++ b/src/cryptoadvance/specter/rpc.py @@ -6,31 +6,29 @@ # TODO: redefine __dir__ and help -RPC_PORTS = { "test": 18332, "regtest": 18443, "main": 8332, 'signet': 38332 } +RPC_PORTS = {"test": 18332, "regtest": 18443, "main": 8332, "signet": 38332} + def get_default_datadir(): """Get default Bitcoin directory depending on the system""" datadir = None - if sys.platform == 'darwin': - datadir = os.path.join(os.environ['HOME'], "Library/Application Support/Bitcoin/") - elif sys.platform == 'win32': - datadir = os.path.join(os.environ['APPDATA'], "Bitcoin") + if sys.platform == "darwin": + datadir = os.path.join( + os.environ["HOME"], "Library/Application Support/Bitcoin/" + ) + elif sys.platform == "win32": + datadir = os.path.join(os.environ["APPDATA"], "Bitcoin") else: - datadir = os.path.join(os.environ['HOME'], ".bitcoin") + datadir = os.path.join(os.environ["HOME"], ".bitcoin") return datadir def get_rpcconfig(datadir=get_default_datadir()): - ''' returns the bitcoin.conf configurations (multiple) in a datastructure - for all networks of a specific datadir. - ''' + """returns the bitcoin.conf configurations (multiple) in a datastructure + for all networks of a specific datadir. + """ config = { - "bitcoin.conf": { - "default": {}, - "main": {}, - "test": {}, - "regtest": {} - }, + "bitcoin.conf": {"default": {}, "main": {}, "test": {}, "regtest": {}}, "cookies": [], } if not os.path.isdir(datadir): # we don't know where to search for files @@ -39,7 +37,7 @@ def get_rpcconfig(datadir=get_default_datadir()): bitcoin_conf_file = os.path.join(datadir, "bitcoin.conf") if os.path.exists(bitcoin_conf_file): try: - with open(bitcoin_conf_file, 'r') as f: + with open(bitcoin_conf_file, "r") as f: current = config["bitcoin.conf"]["default"] for line in f.readlines(): line = line.split("#")[0] @@ -48,9 +46,9 @@ def get_rpcconfig(datadir=get_default_datadir()): if f"[{net}]" in line: current = config["bitcoin.conf"][net] - if '=' not in line: + if "=" not in line: continue - k, v = line.split('=', 1) + k, v = line.split("=", 1) # lines like main.rpcuser and so on if "." in k: net, k = k.split(".", 1) @@ -69,14 +67,10 @@ def get_rpcconfig(datadir=get_default_datadir()): fname = os.path.join(datadir, folders[chain], ".cookie") if os.path.exists(fname): try: - with open(fname, 'r') as f: + with open(fname, "r") as f: content = f.read() user, passwd = content.split(":") - obj = { - "user": user, - "passwd": passwd, - "port": RPC_PORTS[chain] - } + obj = {"user": user, "passwd": passwd, "port": RPC_PORTS[chain]} config["cookies"].append(obj) except: print("Can't open %s file" % fname) @@ -98,7 +92,9 @@ def get_configs(config=None, datadir=get_default_datadir()): if "rpcport" in config["bitcoin.conf"][network]: default["port"] = int(config["bitcoin.conf"][network]["rpcport"]) if "user" in default and "passwd" in default: - if "port" not in config["bitcoin.conf"]["default"]: # only one rpc makes sense in this case + if ( + "port" not in config["bitcoin.conf"]["default"] + ): # only one rpc makes sense in this case if network == "default": continue default["port"] = RPC_PORTS[network] @@ -121,30 +117,36 @@ def detect_rpc_confs(config=None, datadir=get_default_datadir()): rpc_arr.append(conf) return rpc_arr + def detect_rpc_confs_via_env(): - ''' returns an array which might contain one configmap derived from Env-Vars - Env-Vars: BTC_RPC_USER, BTC_RPC_PASSWORD, BTC_RPC_HOST, BTC_RPC_PORT - configmap: {"user":"user","passwd":"password","host":"host","port":"port","protocol":"https"} - ''' + """returns an array which might contain one configmap derived from Env-Vars + Env-Vars: BTC_RPC_USER, BTC_RPC_PASSWORD, BTC_RPC_HOST, BTC_RPC_PORT + configmap: {"user":"user","passwd":"password","host":"host","port":"port","protocol":"https"} + """ rpc_arr = [] - if os.getenv("BTC_RPC_USER") and os.getenv("BTC_RPC_PASSWORD") and \ - os.getenv("BTC_RPC_HOST") and os.getenv("BTC_RPC_PORT") : + if ( + os.getenv("BTC_RPC_USER") + and os.getenv("BTC_RPC_PASSWORD") + and os.getenv("BTC_RPC_HOST") + and os.getenv("BTC_RPC_PORT") + ): logger.info("Detected RPC-Config on Environment-Variables") env_conf = { - "user" : os.getenv("BTC_RPC_USER"), - "passwd" : os.getenv("BTC_RPC_PASSWORD"), - "host" : os.getenv("BTC_RPC_HOST"), - "port" : os.getenv("BTC_RPC_PORT"), - "protocol": os.getenv("BTC_RPC_PROTOCOL","https") # https by default + "user": os.getenv("BTC_RPC_USER"), + "passwd": os.getenv("BTC_RPC_PASSWORD"), + "host": os.getenv("BTC_RPC_HOST"), + "port": os.getenv("BTC_RPC_PORT"), + "protocol": os.getenv("BTC_RPC_PROTOCOL", "https"), # https by default } rpc_arr.append(env_conf) return rpc_arr + def autodetect_rpc_confs(datadir=get_default_datadir(), port=None): - ''' Returns an array of valid and working configurations which - got autodetected. - autodetection checks env-vars and bitcoin-data-dirs - ''' + """Returns an array of valid and working configurations which + got autodetected. + autodetection checks env-vars and bitcoin-data-dirs + """ if port == "": port = None if port is not None: @@ -168,16 +170,18 @@ def autodetect_rpc_confs(datadir=get_default_datadir(), port=None): pass return available_conf_arr + class RpcError(Exception): - ''' Specifically created for error-handling of the BitcoiCore-API - if thrown, check for errors like this: - try: - rpc.does_not_exist() - except RpcError as rpce: - assert rpce.error_code == -32601 - assert rpce.error_msg == "Method not found" - See for error_codes https://github.com/bitcoin/bitcoin/blob/v0.15.0.1/src/rpc/protocol.h#L32L87 - ''' + """Specifically created for error-handling of the BitcoiCore-API + if thrown, check for errors like this: + try: + rpc.does_not_exist() + except RpcError as rpce: + assert rpce.error_code == -32601 + assert rpce.error_msg == "Method not found" + See for error_codes https://github.com/bitcoin/bitcoin/blob/v0.15.0.1/src/rpc/protocol.h#L32L87 + """ + def __init__(self, message, response): super(Exception, self).__init__(message) try: @@ -188,16 +192,27 @@ def __init__(self, message, response): self.status_code = 500 error = response try: - self.error_code = error['error']['code'] - self.error_msg = error['error']['message'] + self.error_code = error["error"]["code"] + self.error_msg = error["error"]["message"] except Exception as e: self.error = "UNKNOWN API-ERROR:%s" % response.text class BitcoinRPC: counter = 0 - def __init__(self, user, passwd, host="127.0.0.1", port=8332, protocol="http", path="", timeout=None, **kwargs): - path = path.replace("//","/") # just in case + + def __init__( + self, + user, + passwd, + host="127.0.0.1", + port=8332, + protocol="http", + path="", + timeout=None, + **kwargs, + ): + path = path.replace("//", "/") # just in case self.user = user self.passwd = passwd self.port = port @@ -208,21 +223,24 @@ def __init__(self, user, passwd, host="127.0.0.1", port=8332, protocol="http", p self.r = None def wallet(self, name=""): - return BitcoinRPC(user=self.user, - passwd=self.passwd, - port=self.port, - protocol=self.protocol, - host=self.host, - path="{}/wallet/{}".format(self.path, name), - timeout=self.timeout + return BitcoinRPC( + user=self.user, + passwd=self.passwd, + port=self.port, + protocol=self.protocol, + host=self.host, + path="{}/wallet/{}".format(self.path, name), + timeout=self.timeout, ) @property def url(self): - return "{s.protocol}://{s.user}:{s.passwd}@{s.host}:{s.port}{s.path}".format(s=self) + return "{s.protocol}://{s.user}:{s.passwd}@{s.host}:{s.port}{s.path}".format( + s=self + ) def test_connection(self): - ''' returns a boolean depending on whether getblockchaininfo() succeeds ''' + """returns a boolean depending on whether getblockchaininfo() succeeds""" try: self.getblockchaininfo() return True @@ -231,8 +249,8 @@ def test_connection(self): def clone(self): """ - Returns a clone of self. - Usefull if you want to mess with the properties + Returns a clone of self. + Usefull if you want to mess with the properties """ return BitcoinRPC( self.user, @@ -241,7 +259,7 @@ def clone(self): self.port, self.protocol, self.path, - self.timeout + self.timeout, ) def multi(self, calls: list, **kwargs): @@ -251,65 +269,59 @@ def multi(self, calls: list, **kwargs): # methods = " ".join(list(dict.fromkeys([call[0] for call in calls]))) # wallet = self.path.split("/")[-1] # print(f"{self.counter}: +{len(calls)} {wallet} {methods}") - headers = {'content-type': 'application/json'} - payload = [{ - "method": method, - "params": args if args != [None] else [], - "jsonrpc": "2.0", - "id": i, - } for i, (method, *args) in enumerate(calls)] + headers = {"content-type": "application/json"} + payload = [ + { + "method": method, + "params": args if args != [None] else [], + "jsonrpc": "2.0", + "id": i, + } + for i, (method, *args) in enumerate(calls) + ] timeout = self.timeout if "timeout" in kwargs: timeout = kwargs["timeout"] url = self.url if "wallet" in kwargs: - url = url+"/wallet/{}".format(kwargs["wallet"]) + url = url + "/wallet/{}".format(kwargs["wallet"]) r = None - if '.onion' in url: + if ".onion" in url: try: requests_session = requests.Session() - requests_session.proxies['http'] = 'socks5h://localhost:9050' - requests_session.proxies['https'] = 'socks5h://localhost:9050' + requests_session.proxies["http"] = "socks5h://localhost:9050" + requests_session.proxies["https"] = "socks5h://localhost:9050" r = requests_session.post( - url, - data=json.dumps(payload), - headers=headers, - timeout=timeout + url, data=json.dumps(payload), headers=headers, timeout=timeout ) except Exception: pass # Tor call failed if r is None: r = requests.post( - url, - data=json.dumps(payload), - headers=headers, - timeout=timeout + url, data=json.dumps(payload), headers=headers, timeout=timeout ) self.r = r if r.status_code != 200: raise RpcError( - "Server responded with error code %d: %s" % ( - r.status_code, r.text - ), r + "Server responded with error code %d: %s" % (r.status_code, r.text), r ) r = r.json() return r def __getattr__(self, method): def fn(*args, **kwargs): - r = self.multi([(method,*args)], **kwargs)[0] + r = self.multi([(method, *args)], **kwargs)[0] if r["error"] is not None: raise RpcError("Request error: %s" % r["error"]["message"], r) return r["result"] + return fn -if __name__ == '__main__': +if __name__ == "__main__": rpc = BitcoinRPC( - "bitcoinrpc", - "foi3uf092ury97iufhjf30982hf928uew9jd209j", - port=18443 + "bitcoinrpc", "foi3uf092ury97iufhjf30982hf928uew9jd209j", port=18443 ) print(rpc.url) @@ -324,8 +336,8 @@ def fn(*args, **kwargs): # or - w = rpc.wallet("") # will load default wallet.dat + w = rpc.wallet("") # will load default wallet.dat print(w.url) - print(w.getbalance()) # now you can run -rpcwallet commands + print(w.getbalance()) # now you can run -rpcwallet commands diff --git a/src/cryptoadvance/specter/server.py b/src/cryptoadvance/specter/server.py index 6f74b6abfc..0a15daedaa 100644 --- a/src/cryptoadvance/specter/server.py +++ b/src/cryptoadvance/specter/server.py @@ -15,7 +15,7 @@ logger = logging.getLogger() -env_path = Path('.') / '.flaskenv' +env_path = Path(".") / ".flaskenv" load_dotenv(env_path) @@ -24,26 +24,24 @@ def create_app(config="cryptoadvance.specter.config.DevelopmentConfig"): if os.environ.get("SPECTER_CONFIG"): config = os.environ.get("SPECTER_CONFIG") - if getattr(sys, 'frozen', False): + if getattr(sys, "frozen", False): # Best understood with the snippet below this section: # https://pyinstaller.readthedocs.io/en/v3.3.1/runtime-information.html#using-sys-executable-and-sys-argv-0 - template_folder = os.path.join(sys._MEIPASS, 'templates') - static_folder = os.path.join(sys._MEIPASS, 'static') + template_folder = os.path.join(sys._MEIPASS, "templates") + static_folder = os.path.join(sys._MEIPASS, "static") logger.info("pyinstaller based instance running in {}".format(sys._MEIPASS)) - app = Flask(__name__, template_folder=template_folder, static_folder=static_folder) - else: app = Flask( - __name__, - template_folder="templates", - static_folder="static" + __name__, template_folder=template_folder, static_folder=static_folder ) + else: + app = Flask(__name__, template_folder="templates", static_folder="static") app.config.from_object(config) return app def init_app(app, hwibridge=False, specter=None): - ''' see blogpost 19nd Feb 2020 ''' + """see blogpost 19nd Feb 2020""" # Login via Flask-Login app.logger.info("Initializing LoginManager") if specter is None: @@ -65,35 +63,39 @@ def login(id): app.login = login # Attach specter instance so child views (e.g. hwi) can access it app.specter = specter - if specter.config.get('auth') == "none": + if specter.config.get("auth") == "none": app.logger.info("Login disabled") app.config["LOGIN_DISABLED"] = True else: app.logger.info("Login enabled") app.logger.info("Initializing Controller ...") - app.register_blueprint(hwi_server, url_prefix='/hwi') + app.register_blueprint(hwi_server, url_prefix="/hwi") if not hwibridge: with app.app_context(): from cryptoadvance.specter import controller + if app.config.get("TESTING") and len(app.view_functions) <= 3: # Need to force a reload as otherwise the import is skipped # in pytest, the app is created anew for ech test # But we shouldn't do that if not necessary as this would result in # --> View function mapping is overwriting an existing endpoint function import importlib + importlib.reload(controller) else: + @app.route("/", methods=["GET"]) def index(): - return redirect('/hwi/settings') + return redirect("/hwi/settings") + return app def create_and_init(): - ''' This method can be used to fill the FLASK_APP-env variable like - export FLASK_APP="src/cryptoadvance/specter/server:create_and_init()" - See Development.md to use this for debugging - ''' + """This method can be used to fill the FLASK_APP-env variable like + export FLASK_APP="src/cryptoadvance/specter/server:create_and_init()" + See Development.md to use this for debugging + """ app = create_app() app.app_context().push() init_app(app) diff --git a/src/cryptoadvance/specter/specter.py b/src/cryptoadvance/specter/specter.py index a31df001ba..d3e59a5e37 100644 --- a/src/cryptoadvance/specter/specter.py +++ b/src/cryptoadvance/specter/specter.py @@ -16,25 +16,37 @@ logger = logging.getLogger(__name__) + def get_rpc(conf): if "autodetect" not in conf: conf["autodetect"] = True if conf["autodetect"]: if "port" in conf: - rpc_conf_arr = autodetect_rpc_confs(datadir=os.path.expanduser(conf["datadir"]), port=conf["port"]) + rpc_conf_arr = autodetect_rpc_confs( + datadir=os.path.expanduser(conf["datadir"]), port=conf["port"] + ) else: - rpc_conf_arr = autodetect_rpc_confs(datadir=os.path.expanduser(conf["datadir"])) + rpc_conf_arr = autodetect_rpc_confs( + datadir=os.path.expanduser(conf["datadir"]) + ) if len(rpc_conf_arr) > 0: rpc = BitcoinRPC(**rpc_conf_arr[0]) else: return None else: - rpc = BitcoinRPC(conf["user"], conf["password"], - host=conf["host"], port=conf["port"], protocol=conf["protocol"]) + rpc = BitcoinRPC( + conf["user"], + conf["password"], + host=conf["host"], + port=conf["port"], + protocol=conf["protocol"], + ) return rpc + class Specter: - ''' A central Object mostly holding app-settings ''' + """A central Object mostly holding app-settings""" + CONFIG_FILE_NAME = "config.json" # use this lock for all fs operations lock = threading.Lock() @@ -61,16 +73,11 @@ def __init__(self, data_folder="./data", config={}): "user": "", "password": "", "port": "", - "host": "localhost", # localhost - "protocol": "http" # https for the future + "host": "localhost", # localhost + "protocol": "http", # https for the future }, "auth": "none", - "explorers": { - "main": "", - "test": "", - "regtest": "", - "signet": "" - }, + "explorers": {"main": "", "test": "", "regtest": "", "signet": ""}, "hwi_bridge_url": "/hwi/api/", # unique id that will be used in wallets path in Bitcoin Core # empty by default for backward-compatibility @@ -89,8 +96,8 @@ def __init__(self, data_folder="./data", config={}): @property def bitcoin_datadir(self): - if 'datadir' in self.config['rpc']: - return os.path.expanduser(self.config['rpc']['datadir']) + if "datadir" in self.config["rpc"]: + return os.path.expanduser(self.config["rpc"]["datadir"]) return get_default_datadir() def check(self, user=current_user): @@ -103,21 +110,22 @@ def check(self, user=current_user): # otherwise - create one and assign unique id else: if self.config["uid"] == "": - self.config["uid"] = random.randint( - 0, 256 ** 8 - ).to_bytes(8, 'big').hex() + self.config["uid"] = ( + random.randint(0, 256 ** 8).to_bytes(8, "big").hex() + ) self._save() # init arguments deep_update(self.config, self.arg_config) # override loaded config self.rpc = get_rpc(self.config["rpc"]) - self._is_configured = (self.rpc is not None) + self._is_configured = self.rpc is not None self._is_running = False if self._is_configured: try: res = [ - r["result"] for r in self.rpc.multi( + r["result"] + for r in self.rpc.multi( [ ("getblockchaininfo", None), ("getnetworkinfo", None), @@ -130,61 +138,73 @@ def check(self, user=current_user): ] self._info = res[0] self._network_info = res[1] - self._info['mempool_info'] = res[2] - self._info['uptime'] = res[3] + self._info["mempool_info"] = res[2] + self._info["uptime"] = res[3] try: self.rpc.getblockfilter(res[4]) - self._info['blockfilterindex'] = True + self._info["blockfilterindex"] = True except: - self._info['blockfilterindex'] = False - self._info["utxorescan"] = (res[5]["progress"] - if res[5] is not None and "progress" in res[5] - else None) + self._info["blockfilterindex"] = False + self._info["utxorescan"] = ( + res[5]["progress"] + if res[5] is not None and "progress" in res[5] + else None + ) if self._info["utxorescan"] is None: self.utxorescanwallet = None self._is_running = True except Exception as e: self._info = {"chain": None} - self._network_info = {"subversion": '', "version": 999999} + self._network_info = {"subversion": "", "version": 999999} logger.error("Exception %s while specter.check()" % e) pass else: self._info = {"chain": None} - self._network_info = {"subversion": '', "version": 999999} + self._network_info = {"subversion": "", "version": 999999} if not self._is_running: self._info["chain"] = None chain = self._info["chain"] - if hasattr(user, 'is_admin'): - user_folder_id = '_' + user.id if user and not user.is_admin else '' + if hasattr(user, "is_admin"): + user_folder_id = "_" + user.id if user and not user.is_admin else "" else: - user_folder_id = '' + user_folder_id = "" - if self.config['auth'] != 'usernamepassword' or (user and not user.is_anonymous): + if self.config["auth"] != "usernamepassword" or ( + user and not user.is_anonymous + ): if self.device_manager is None: - self.device_manager = DeviceManager(os.path.join(self.data_folder, "devices{}".format(user_folder_id))) + self.device_manager = DeviceManager( + os.path.join(self.data_folder, "devices{}".format(user_folder_id)) + ) else: - self.device_manager.update(data_folder=os.path.join(self.data_folder, "devices{}".format(user_folder_id))) + self.device_manager.update( + data_folder=os.path.join( + self.data_folder, "devices{}".format(user_folder_id) + ) + ) wallets_path = "specter%s" % self.config["uid"] # if chain, user or data folder changed - if (self.wallet_manager is None + if ( + self.wallet_manager is None or self.wallet_manager.data_folder != self.data_folder or self.wallet_manager.rpc_path != wallets_path - or self.wallet_manager.chain != chain): + or self.wallet_manager.chain != chain + ): self.wallet_manager = WalletManager( - os.path.join(self.data_folder, "wallets{}".format(user_folder_id)), - self.rpc, + os.path.join(self.data_folder, "wallets{}".format(user_folder_id)), + self.rpc, chain, self.device_manager, - path=wallets_path + path=wallets_path, ) else: self.wallet_manager.update( - os.path.join(self.data_folder, "wallets{}".format(user_folder_id)), - self.rpc, - chain=chain + os.path.join(self.data_folder, "wallets{}".format(user_folder_id)), + self.rpc, + chain=chain, ) def abortrescanutxo(self): @@ -205,32 +225,34 @@ def test_rpc(self, **kwargs): if rpc is None: return {"out": "", "err": "autodetect failed", "code": -1} r = {} - r['tests'] = {} + r["tests"] = {} try: - r['tests']['recent_version'] = int(rpc.getnetworkinfo()['version']) >= 170000 - r['tests']['connectable'] = True - r['tests']['credentials'] = True + r["tests"]["recent_version"] = ( + int(rpc.getnetworkinfo()["version"]) >= 170000 + ) + r["tests"]["connectable"] = True + r["tests"]["credentials"] = True try: rpc.listwallets() - r['tests']['wallets'] = True + r["tests"]["wallets"] = True except RpcError as rpce: logger.error(rpce) - if rpce.status_code == 404: - r['tests']['wallets'] = False + if rpce.status_code == 404: + r["tests"]["wallets"] = False else: raise rpce - r["out"] = json.dumps(rpc.getblockchaininfo(),indent=4) + r["out"] = json.dumps(rpc.getblockchaininfo(), indent=4) r["err"] = "" r["code"] = 0 except ConnectionError as e: logger.error(e) - r['tests']['connectable'] = False + r["tests"]["connectable"] = False r["err"] = "Failed to connect!" r["code"] = -1 except RpcError as rpce: logger.error(rpce) - if rpce.status_code == 401: - r['tests']['credentials'] = False + if rpce.status_code == 401: + r["tests"]["credentials"] = False else: raise rpce except Exception as e: @@ -260,13 +282,13 @@ def update_rpc(self, **kwargs): self.check() def update_auth(self, auth): - ''' simply persisting the current auth-choice ''' + """simply persisting the current auth-choice""" if self.config["auth"] != auth: self.config["auth"] = auth self._save() def update_explorer(self, explorer, user): - ''' update the block explorers urls ''' + """update the block explorers urls""" # we don't know what chain to change if not self.chain: return @@ -274,7 +296,7 @@ def update_explorer(self, explorer, user): # make sure the urls end with a "/" explorer += "/" # update the urls in the app config - if user.id == 'admin': + if user.id == "admin": if self.config["explorers"][self.chain] != explorer: self.config["explorers"][self.chain] = explorer self._save() @@ -282,14 +304,14 @@ def update_explorer(self, explorer, user): user.set_explorer(self, explorer) def update_hwi_bridge_url(self, url, user): - ''' update the hwi bridge url to use ''' + """update the hwi bridge url to use""" if url and not url.endswith("/"): # make sure the urls end with a "/" url += "/" # a few dummy checks: # no schema and not local if "://" not in url and not url.startswith("/"): - url = "http://"+url + url = "http://" + url # wrong ending: if url.endswith("/hwi/settings/"): url = url.replace("/hwi/settings/", "/hwi/api/") @@ -297,42 +319,42 @@ def update_hwi_bridge_url(self, url, user): if not url.endswith("/hwi/api/"): url += "hwi/api/" - if user.id == 'admin': + if user.id == "admin": self.config["hwi_bridge_url"] = url self._save() else: user.set_hwi_bridge_url(self, url) def update_unit(self, unit, user): - if user.id == 'admin': + if user.id == "admin": self.config["unit"] = unit self._save() else: user.set_unit(self, unit) def update_merkleproof_settings(self, validate_bool): - if validate_bool is True and self._info.get('pruned') is True: + if validate_bool is True and self._info.get("pruned") is True: validate_bool = False logger.warning("Cannot enable merkleproof setting on pruned node.") - self.config['validate_merkle_proofs'] = validate_bool + self.config["validate_merkle_proofs"] = validate_bool self._save() def add_new_user_otp(self, otp_dict): - ''' adds an OTP for user registration ''' - if 'new_user_otps' not in self.config: - self.config['new_user_otps'] = [] - self.config['new_user_otps'].append(otp_dict) + """adds an OTP for user registration""" + if "new_user_otps" not in self.config: + self.config["new_user_otps"] = [] + self.config["new_user_otps"].append(otp_dict) self._save() def burn_new_user_otp(self, otp): - ''' validates an OTP for user registration and removes it if valid''' - if 'new_user_otps' not in self.config: - return False - for i, otp_dict in enumerate(self.config['new_user_otps']): + """validates an OTP for user registration and removes it if valid""" + if "new_user_otps" not in self.config: + return False + for i, otp_dict in enumerate(self.config["new_user_otps"]): # TODO: Validate OTP did not expire based on created_at - if otp_dict['otp'] == int(otp): - del self.config['new_user_otps'][i] + if otp_dict["otp"] == int(otp): + del self.config["new_user_otps"][i] self._save() return True return False @@ -389,13 +411,7 @@ def network_info(self): @property def bitcoin_core_version(self): - return self.network_info['subversion'].replace( - '/', - '' - ).replace( - 'Satoshi:', - '' - ) + return self.network_info["subversion"].replace("/", "").replace("Satoshi:", "") @property def chain(self): @@ -410,7 +426,10 @@ def explorer(self): else: return "" else: - if "explorers" in current_user.config and self.chain in current_user.config["explorers"]: + if ( + "explorers" in current_user.config + and self.chain in current_user.config["explorers"] + ): return current_user.config["explorers"][self.chain] else: return "" @@ -445,24 +464,22 @@ def unit(self): def specter_backup_file(self): memory_file = BytesIO() - with zipfile.ZipFile(memory_file, 'w') as zf: + with zipfile.ZipFile(memory_file, "w") as zf: if self.wallet_manager: for wallet in self.wallet_manager.wallets.values(): - data = zipfile.ZipInfo('{}.json'.format(wallet.alias)) + data = zipfile.ZipInfo("{}.json".format(wallet.alias)) data.date_time = time.localtime(time.time())[:6] data.compress_type = zipfile.ZIP_DEFLATED zf.writestr( - 'wallets/{}.json'.format(wallet.alias), - json.dumps(wallet.json) + "wallets/{}.json".format(wallet.alias), json.dumps(wallet.json) ) if self.device_manager: for device in self.device_manager.devices.values(): - data = zipfile.ZipInfo('{}.json'.format(device.alias)) + data = zipfile.ZipInfo("{}.json".format(device.alias)) data.date_time = time.localtime(time.time())[:6] data.compress_type = zipfile.ZIP_DEFLATED zf.writestr( - 'devices/{}.json'.format(device.alias), - json.dumps(device.json) + "devices/{}.json".format(device.alias), json.dumps(device.json) ) memory_file.seek(0) return memory_file @@ -475,4 +492,3 @@ def specter_version(self): if not self._current_version: self._current_version = get_version_info()[0] return self._current_version - diff --git a/src/cryptoadvance/specter/specter_error.py b/src/cryptoadvance/specter/specter_error.py index a453e23f28..893481f488 100644 --- a/src/cryptoadvance/specter/specter_error.py +++ b/src/cryptoadvance/specter/specter_error.py @@ -1,3 +1,4 @@ class SpecterError(Exception): - ''' A SpecterError contains meaningfull messages which can be passed directly to the user ''' - pass \ No newline at end of file + """A SpecterError contains meaningfull messages which can be passed directly to the user""" + + pass diff --git a/src/cryptoadvance/specter/user.py b/src/cryptoadvance/specter/user.py index d08417d269..20a610bc5b 100644 --- a/src/cryptoadvance/specter/user.py +++ b/src/cryptoadvance/specter/user.py @@ -7,28 +7,38 @@ from .specter_error import SpecterError from .helpers import fslock + def hash_password(password): """Hash a password for storing.""" salt = binascii.b2a_base64(hashlib.sha256(os.urandom(60)).digest()).strip() - pwdhash = binascii.b2a_base64(hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 10000)).strip().decode() - return { 'salt': salt.decode(), 'pwdhash': pwdhash } + pwdhash = ( + binascii.b2a_base64( + hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 10000) + ) + .strip() + .decode() + ) + return {"salt": salt.decode(), "pwdhash": pwdhash} def verify_password(stored_password, provided_password): """Verify a stored password against one provided by user""" - pwdhash = hashlib.pbkdf2_hmac('sha256', - provided_password.encode('utf-8'), - stored_password['salt'].encode(), - 10000) - return pwdhash == binascii.a2b_base64(stored_password['pwdhash']) + pwdhash = hashlib.pbkdf2_hmac( + "sha256", + provided_password.encode("utf-8"), + stored_password["salt"].encode(), + 10000, + ) + return pwdhash == binascii.a2b_base64(stored_password["pwdhash"]) + def get_users_json(specter): users = [ { - 'id': 'admin', - 'username': 'admin', - 'password': hash_password('admin'), - 'is_admin': True + "id": "admin", + "username": "admin", + "password": hash_password("admin"), + "is_admin": True, } ] @@ -45,7 +55,7 @@ def get_users_json(specter): def save_users_json(specter, users): with fslock: - with open(os.path.join(specter.data_folder, 'users.json'), "w") as f: + with open(os.path.join(specter.data_folder, "users.json"), "w") as f: json.dump(users, f, indent=4) @@ -61,12 +71,23 @@ def __init__(self, id, username, password, config, is_admin=False): def from_json(cls, user_dict): # TODO: Unify admin in backwards compatible way try: - if not user_dict['is_admin']: - return cls(user_dict['id'], user_dict['username'], user_dict['password'], user_dict['config']) + if not user_dict["is_admin"]: + return cls( + user_dict["id"], + user_dict["username"], + user_dict["password"], + user_dict["config"], + ) else: - return cls(user_dict['id'], user_dict['username'], user_dict['password'], {}, is_admin=True) + return cls( + user_dict["id"], + user_dict["username"], + user_dict["password"], + {}, + is_admin=True, + ) except: - raise SpecterError('Unable to parse user JSON.') + raise SpecterError("Unable to parse user JSON.") @classmethod def get_user(cls, specter, id): @@ -96,20 +117,20 @@ def get_all_users(cls, specter): @property def json(self): user_dict = { - 'id': self.id, - 'username': self.username, - 'password': self.password, - 'is_admin': self.is_admin + "id": self.id, + "username": self.username, + "password": self.password, + "is_admin": self.is_admin, } if not self.is_admin: - user_dict['config'] = self.config + user_dict["config"] = self.config return user_dict def save_info(self, specter, delete=False): users = get_users_json(specter) existing = False for i in range(len(users)): - if users[i]['id'] == self.id: + if users[i]["id"] == self.id: if not delete: users[i] = self.json existing = True @@ -122,20 +143,24 @@ def save_info(self, specter, delete=False): save_users_json(specter, users) def set_explorer(self, specter, explorer): - self.config['explorers'][specter.chain] = explorer + self.config["explorers"][specter.chain] = explorer self.save_info(specter) def set_hwi_bridge_url(self, specter, url): - self.config['hwi_bridge_url'] = url + self.config["hwi_bridge_url"] = url self.save_info(specter) def set_unit(self, specter, unit): - self.config['unit'] = unit + self.config["unit"] = unit self.save_info(specter) def delete(self, specter): - devices_datadir_path = os.path.join(os.path.join(specter.data_folder, "devices_{}".format(self.id))) - wallets_datadir_path = os.path.join(os.path.join(specter.data_folder, "wallets_{}".format(self.id))) + devices_datadir_path = os.path.join( + os.path.join(specter.data_folder, "devices_{}".format(self.id)) + ) + wallets_datadir_path = os.path.join( + os.path.join(specter.data_folder, "wallets_{}".format(self.id)) + ) if os.path.exists(devices_datadir_path): shutil.rmtree(devices_datadir_path) if os.path.exists(wallets_datadir_path): diff --git a/src/cryptoadvance/specter/util/base43.py b/src/cryptoadvance/specter/util/base43.py index 05ea0b39ce..9f6409fee2 100644 --- a/src/cryptoadvance/specter/util/base43.py +++ b/src/cryptoadvance/specter/util/base43.py @@ -8,14 +8,14 @@ def b43_encode(b: bytes) -> str: """Encode bytes to a base58-encoded string""" # Convert big-endian bytes to integer - n: int = int('0x0' + hexlify(b).decode('utf8'), 16) + n: int = int("0x0" + hexlify(b).decode("utf8"), 16) # Divide that integer into base58 temp: List[str] = [] while n > 0: n, r = divmod(n, 43) temp.append(BASE43_CHARS[r]) - res: str = ''.join(temp[::-1]) + res: str = "".join(temp[::-1]) # Encode leading zeros as base58 zeros czero: int = 0 @@ -31,23 +31,22 @@ def b43_encode(b: bytes) -> str: def b43_decode(s: str) -> bytes: """Decode a base58-encoding string, returning bytes""" if not s: - return b'' + return b"" # Convert the string to an integer n: int = 0 for c in s: n *= 43 if c not in BASE43_CHARS: - raise ValueError( - 'Character %r is not a valid base43 character' % c) + raise ValueError("Character %r is not a valid base43 character" % c) digit = BASE43_CHARS.index(c) n += digit # Convert the integer to bytes - h: str = '%x' % n + h: str = "%x" % n if len(h) % 2: - h = '0' + h - res = unhexlify(h.encode('utf8')) + h = "0" + h + res = unhexlify(h.encode("utf8")) # Add padding back. pad = 0 @@ -56,4 +55,4 @@ def b43_decode(s: str) -> bytes: pad += 1 else: break - return b'\x00' * pad + res + return b"\x00" * pad + res diff --git a/src/cryptoadvance/specter/util/base58.py b/src/cryptoadvance/specter/util/base58.py index 2d152aac0c..7f02df6e38 100644 --- a/src/cryptoadvance/specter/util/base58.py +++ b/src/cryptoadvance/specter/util/base58.py @@ -1,6 +1,6 @@ import hashlib -BASE58_ALPHABET = b'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +BASE58_ALPHABET = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" def double_sha256(s): @@ -15,9 +15,9 @@ def encode_base58(s): count += 1 else: break - prefix = b'1' * count + prefix = b"1" * count # convert from binary to hex, then hex to integer - num = int.from_bytes(s, 'big') + num = int.from_bytes(s, "big") result = bytearray() while num > 0: num, mod = divmod(num, 58) @@ -27,20 +27,21 @@ def encode_base58(s): def encode_base58_checksum(s): - return encode_base58(s + double_sha256(s)[:4]).decode('ascii') + return encode_base58(s + double_sha256(s)[:4]).decode("ascii") def decode_base58(s, num_bytes=82, strip_leading_zeros=False): num = 0 - for c in s.encode('ascii'): + for c in s.encode("ascii"): num *= 58 num += BASE58_ALPHABET.index(c) - combined = num.to_bytes(num_bytes, byteorder='big') + combined = num.to_bytes(num_bytes, byteorder="big") if strip_leading_zeros: while combined[0] == 0: combined = combined[1:] checksum = combined[-4:] if double_sha256(combined[:-4])[:4] != checksum: - raise ValueError('bad address: {} {}'.format( - checksum, double_sha256(combined)[:4])) + raise ValueError( + "bad address: {} {}".format(checksum, double_sha256(combined)[:4]) + ) return combined[:-4] diff --git a/src/cryptoadvance/specter/util/bcur.py b/src/cryptoadvance/specter/util/bcur.py index 49aa98c188..c4ef15845f 100644 --- a/src/cryptoadvance/specter/util/bcur.py +++ b/src/cryptoadvance/specter/util/bcur.py @@ -4,17 +4,19 @@ CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + def bech32_polymod(values): """Internal function that computes the Bech32 checksum.""" - generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3] chk = 1 for value in values: top = chk >> 25 - chk = (chk & 0x1ffffff) << 5 ^ value + chk = (chk & 0x1FFFFFF) << 5 ^ value for i in range(5): chk ^= generator[i] if ((top >> i) & 1) else 0 return chk + def bech32_hrp_expand(hrp): """Expand the HRP into values for checksum computation.""" return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] @@ -35,22 +37,23 @@ def bech32_create_checksum(hrp, data): def bech32_encode(hrp, data): """Compute a Bech32 string given HRP and data values.""" combined = data + bech32_create_checksum(hrp, data) - return hrp + '1' + ''.join([CHARSET[d] for d in combined]) + return hrp + "1" + "".join([CHARSET[d] for d in combined]) def bech32_decode(bech): """Validate a Bech32 string, and determine HRP and data.""" - if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or - (bech.lower() != bech and bech.upper() != bech)): + if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or ( + bech.lower() != bech and bech.upper() != bech + ): return (None, None) bech = bech.lower() - pos = bech.rfind('1') + pos = bech.rfind("1") if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: return (None, None) - if not all(x in CHARSET for x in bech[pos+1:]): + if not all(x in CHARSET for x in bech[pos + 1 :]): return (None, None) hrp = bech[:pos] - data = [CHARSET.find(x) for x in bech[pos+1:]] + data = [CHARSET.find(x) for x in bech[pos + 1 :]] if not bech32_verify_checksum(hrp, data): return (None, None) return (hrp, data[:-6]) @@ -101,60 +104,65 @@ def encode(hrp, witver, witprog): return None return ret -def bc32encode(data:bytes)->str: + +def bc32encode(data: bytes) -> str: """ - bc32 encoding + bc32 encoding see https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-004-bc32.md """ dd = convertbits(data, 8, 5) - polymod = bech32_polymod([0] + dd + [0, 0, 0, 0, 0, 0]) ^ 0x3fffffff + polymod = bech32_polymod([0] + dd + [0, 0, 0, 0, 0, 0]) ^ 0x3FFFFFFF chk = [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] - return ''.join([CHARSET[d] for d in dd+chk]) + return "".join([CHARSET[d] for d in dd + chk]) -def bc32decode(bc32:str)->bytes: + +def bc32decode(bc32: str) -> bytes: """ - bc32 decoding + bc32 decoding see https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-004-bc32.md """ - if (bc32.lower() != bc32 and bc32.upper() != bc32): + if bc32.lower() != bc32 and bc32.upper() != bc32: return None bc32 = bc32.lower() if not all([x in CHARSET for x in bc32]): return None res = [CHARSET.find(c) for c in bc32.lower()] - if bech32_polymod([0] + res)!=0x3fffffff: + if bech32_polymod([0] + res) != 0x3FFFFFFF: return None - return bytes(convertbits(res[:-6],5,8,False)) + return bytes(convertbits(res[:-6], 5, 8, False)) + def cbor_encode(data): l = len(data) - if l<=23: - prefix = bytes([0x40+l]) - elif l<=255: + if l <= 23: + prefix = bytes([0x40 + l]) + elif l <= 255: prefix = bytes([0x58, l]) - elif l<=65535: - prefix = b'\x59'+l.to_bytes(2, 'big') + elif l <= 65535: + prefix = b"\x59" + l.to_bytes(2, "big") else: - prefix = b'\x60'+l.to_bytes(4, 'big') - return prefix+data + prefix = b"\x60" + l.to_bytes(4, "big") + return prefix + data + def cbor_decode(data): s = BytesIO(data) b = s.read(1)[0] if b >= 0x40 and b < 0x58: - l = b-0x40 + l = b - 0x40 return s.read(l) if b == 0x58: l = s.read(1)[0] return s.read(l) if b == 0x59: - l = int.from_bytes(s.read(2),'big') + l = int.from_bytes(s.read(2), "big") return s.read(l) if b == 0x60: - l = int.from_bytes(s.read(4),'big') + l = int.from_bytes(s.read(4), "big") return s.read(l) return None + def bcur_encode(data): """Returns bcur encoded string and hash digest""" cbor = cbor_encode(data) @@ -163,6 +171,7 @@ def bcur_encode(data): enc_hash = bc32encode(h) return enc, enc_hash + def bcur_decode(data, checksum=None): """Returns decoded data, verifies hash digest if provided""" cbor = bc32decode(data) @@ -171,18 +180,20 @@ def bcur_decode(data, checksum=None): assert h == hashlib.sha256(cbor).digest() return cbor_decode(cbor) -if __name__ == '__main__': + +if __name__ == "__main__": from binascii import a2b_base64 + b64 = "cHNidP8BAHEBAAAAAfPQ5Rpeu5nH0TImK4Sbu9lxIOGEynRadywPxaPyhnTwAAAAAAD/////AkoRAAAAAAAAFgAUFCYoQzGSRmYVAuZNuXF0OrPg9jWIEwAAAAAAABYAFOZMlwM1sZGLivwOcOh77amAlvD5AAAAAAABAR+tKAAAAAAAABYAFM4u9V5WG+Fe9l3MefmYEX4ULWAWIgYDA+jO+oOuN37ABK67BA/+SuuR/57c7OkyfyR7hR34FDsYccBxUlQAAIAAAACAAAAAgAAAAAAFAAAAACICApJMZBvzWiavLN7nievKQoylwPoffLkXZUIgGHF4HgwaGHHAcVJUAACAAAAAgAAAAIABAAAACwAAAAAA" raw = a2b_base64(b64) enc, enc_hash = bcur_encode(raw) dec = bcur_decode(enc, enc_hash) - assert (dec == raw) + assert dec == raw testres = [ "tyq3wurnvf607qgqwyqsqqqqq8eapeg6t6aen373xgnzhpymh0vhzg8psn98gknh9s8utgljse60qqqqqqqqpllllllsyjs3qqqqqqqqqqtqq9q5yc5yxvvjgenp2qhxfkuhzap6k0s0vdvgzvqqqqqqqqqpvqq5uexfwqe4kxgchzhupecws7ld4xqfdu8eqqqqqqqq", "qyq3ltfgqqqqqqqqqqtqq9xw9m64u4smu900vhwv08uesyt7zskkq93zqcps86xwl2p6udm7cqz2awcyplly46u3l70dem8fxfljg7u9rhupgwccw8q8z5j5qqqgqqqqqzqqqqqqsqqqqqqqq5qqqqqqygpq9yjvvsdlxk3x4ukdaeufa09y9r99crap7l9ezaj5ygqc", - "w9upurq6rpcuqu2j2sqqpqqqqqqgqqqqqzqqzqqqqq9sqqqqqqqqmkdau4" + "w9upurq6rpcuqu2j2sqqpqqqqqqgqqqqqzqqzqqqqq9sqqqqqqqqmkdau4", ] testhash = "hlwjxjx550k4nnfdl5py2tn3vnh6g60slnw5dmld6ktrkkz200as49spg5" - assert (enc_hash == testhash) - assert (enc=="".join(testres)) + assert enc_hash == testhash + assert enc == "".join(testres) diff --git a/src/cryptoadvance/specter/util/descriptor.py b/src/cryptoadvance/specter/util/descriptor.py index ade8d2b12c..6e98703717 100644 --- a/src/cryptoadvance/specter/util/descriptor.py +++ b/src/cryptoadvance/specter/util/descriptor.py @@ -2,21 +2,23 @@ # From: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp + def PolyMod(c, val): c0 = c >> 35 - c = ((c & 0x7ffffffff) << 5) ^ val - if (c0 & 1): - c ^= 0xf5dee51989 - if (c0 & 2): - c ^= 0xa9fdca3312 - if (c0 & 4): - c ^= 0x1bab10e32d - if (c0 & 8): - c ^= 0x3706b1677a - if (c0 & 16): - c ^= 0x644d626ffd + c = ((c & 0x7FFFFFFFF) << 5) ^ val + if c0 & 1: + c ^= 0xF5DEE51989 + if c0 & 2: + c ^= 0xA9FDCA3312 + if c0 & 4: + c ^= 0x1BAB10E32D + if c0 & 8: + c ^= 0x3706B1677A + if c0 & 16: + c ^= 0x644D626FFD return c + def DescriptorChecksum(desc): INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ " CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" @@ -44,11 +46,13 @@ def DescriptorChecksum(desc): ret = [None] * 8 for j in range(0, 8): ret[j] = CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31] - return ''.join(ret) + return "".join(ret) + def AddChecksum(desc): return desc + "#" + DescriptorChecksum(desc) + class Descriptor: def __init__( self, @@ -63,7 +67,7 @@ def __init__( sh_wsh=None, wsh=None, multisig_M=None, - multisig_N=None + multisig_N=None, ): self.origin_fingerprint = origin_fingerprint self.origin_path = origin_path @@ -99,7 +103,7 @@ def parse(cls, desc, testnet=False): multisig_N = None # Check the checksum - check_split = desc.split('#') + check_split = desc.split("#") if len(check_split) > 2: return None if len(check_split) == 2: @@ -124,17 +128,17 @@ def parse(cls, desc, testnet=False): sh = True if sh or sh_wsh or wsh: - if 'multi(' not in desc: + if "multi(" not in desc: # only multisig scripts are supported return None # get the list of keys only - keys = desc.split(',', 1)[1].split(')', 1)[0].split(',') - if 'sortedmulti' in desc: - keys.sort(key=lambda x: x if ']' not in x else x.split(']')[1]) - multisig_M = desc.split(',')[0].split('(')[-1] + keys = desc.split(",", 1)[1].split(")", 1)[0].split(",") + if "sortedmulti" in desc: + keys.sort(key=lambda x: x if "]" not in x else x.split("]")[1]) + multisig_M = desc.split(",")[0].split("(")[-1] multisig_N = len(keys) else: - keys = [desc.split('(')[-1].split(')', 1)[0]] + keys = [desc.split("(")[-1].split(")", 1)[0]] descriptors = [] for key in keys: @@ -146,10 +150,10 @@ def parse(cls, desc, testnet=False): origin_fingerprint = match.group(1) origin_path = match.group(2) # Replace h with ' - origin_path = origin_path.replace('h', '\'') + origin_path = origin_path.replace("h", "'") else: origin_fingerprint = origin - origin_path = '' + origin_path = "" base_key_and_path_match = re.search(r"\[.*\](\w+)([\d'\/\*]*)", key) else: @@ -158,13 +162,26 @@ def parse(cls, desc, testnet=False): if base_key_and_path_match: base_key = base_key_and_path_match.group(1) path_suffix = base_key_and_path_match.group(2) - if path_suffix == '': + if path_suffix == "": path_suffix = None else: if origin_match is None: return None - descriptors.append(cls(origin_fingerprint, origin_path, base_key, path_suffix, testnet, sh_wpkh, wpkh, sh, sh_wsh, wsh)) + descriptors.append( + cls( + origin_fingerprint, + origin_path, + base_key, + path_suffix, + testnet, + sh_wpkh, + wpkh, + sh, + sh_wsh, + wsh, + ) + ) if len(descriptors) == 1: return descriptors[0] else: @@ -181,28 +198,30 @@ def parse(cls, desc, testnet=False): sh_wsh, wsh, multisig_M, - multisig_N + multisig_N, ) def serialize(self): - descriptor_open = 'pkh(' - descriptor_close = ')' - origin = '' - path_suffix = '' + descriptor_open = "pkh(" + descriptor_close = ")" + origin = "" + path_suffix = "" if self.wpkh: - descriptor_open = 'wpkh(' + descriptor_open = "wpkh(" elif self.sh_wpkh: - descriptor_open = 'sh(wpkh(' - descriptor_close = '))' + descriptor_open = "sh(wpkh(" + descriptor_close = "))" elif self.sh or self.sh_wsh or self.wsh: # serialize multisig descriptor is not supported yet. return None if self.origin_fingerprint and self.origin_path: - origin = '[' + self.origin_fingerprint + self.origin_path + ']' + origin = "[" + self.origin_fingerprint + self.origin_path + "]" if self.path_suffix: path_suffix = self.path_suffix - return AddChecksum(descriptor_open + origin + self.base_key + path_suffix + descriptor_close) + return AddChecksum( + descriptor_open + origin + self.base_key + path_suffix + descriptor_close + ) diff --git a/src/cryptoadvance/specter/util/json_rpc.py b/src/cryptoadvance/specter/util/json_rpc.py index 5d824321d6..9f1b3ef000 100644 --- a/src/cryptoadvance/specter/util/json_rpc.py +++ b/src/cryptoadvance/specter/util/json_rpc.py @@ -6,6 +6,7 @@ class JSONRPC: Base JSON-RPC class. Add methods to self.exposed_rpc to make it available with jsonrpc() call. """ + def __init__(self): self.exposed_rpc = {} @@ -14,22 +15,25 @@ def jsonrpc(self, request): # if it is a list (not bundled) run one by one if isinstance(request, list): return [self.jsonrpc(req) for req in request] - response = { "jsonrpc": "2.0", "id": request["id"] if "id" in request else None } + response = {"jsonrpc": "2.0", "id": request["id"] if "id" in request else None} if "method" not in request: - response["error"] = { "code": -32600, "message": "Invalid Request. Request must specify a 'method'." } + response["error"] = { + "code": -32600, + "message": "Invalid Request. Request must specify a 'method'.", + } return response if request["method"] not in self.exposed_rpc: - response["error"] = { "code": -32601, "message": "Method not found." } + response["error"] = {"code": -32601, "message": "Method not found."} return response method = self.exposed_rpc[request["method"]] try: if "params" not in request: response["result"] = method() elif isinstance(request["params"], list): - response["result"] = method(*request["params"]) # list -> *args + response["result"] = method(*request["params"]) # list -> *args else: - response["result"] = method(**request["params"]) # dict -> **kwargs + response["result"] = method(**request["params"]) # dict -> **kwargs except Exception as e: traceback.print_exc() - response["error"] = { "code": -32000, "message": f"Internal error: {e}." } + response["error"] = {"code": -32000, "message": f"Internal error: {e}."} return response diff --git a/src/cryptoadvance/specter/util/merkleblock.py b/src/cryptoadvance/specter/util/merkleblock.py index 5aca96a648..4a183a0289 100644 --- a/src/cryptoadvance/specter/util/merkleblock.py +++ b/src/cryptoadvance/specter/util/merkleblock.py @@ -10,15 +10,15 @@ def hash256(s): def read_varint(s): - '''read_varint reads a variable integer from a stream''' + """read_varint reads a variable integer from a stream""" i = s.read(1)[0] - if i == 0xfd: + if i == 0xFD: # 0xfd means the next two bytes are the number return little_endian_to_int(s.read(2)) - elif i == 0xfe: + elif i == 0xFE: # 0xfe means the next four bytes are the number return little_endian_to_int(s.read(4)) - elif i == 0xff: + elif i == 0xFF: # 0xff means the next eight bytes are the number return little_endian_to_int(s.read(8)) else: @@ -27,17 +27,17 @@ def read_varint(s): def merkle_parent(hash1, hash2): - '''Takes the binary hashes and calculates the hash256''' + """Takes the binary hashes and calculates the hash256""" # return the hash256 of hash1 + hash2 return hash256(hash1 + hash2) def merkle_parent_level(hashes): - '''Takes a list of binary hashes and returns a list that's half - the length''' + """Takes a list of binary hashes and returns a list that's half + the length""" # if the list has exactly 1 element raise an error if len(hashes) == 1: - raise RuntimeError('Cannot take a parent level with only 1 item') + raise RuntimeError("Cannot take a parent level with only 1 item") # if the list has an odd number of elements, duplicate the last one # and put it at the end so it has an even number of elements if len(hashes) % 2 == 1: @@ -55,8 +55,7 @@ def merkle_parent_level(hashes): def merkle_root(hashes): - '''Takes a list of binary hashes and returns the merkle root - ''' + """Takes a list of binary hashes and returns the merkle root""" # current level starts as hashes current_level = hashes # loop until there's exactly 1 element @@ -68,17 +67,17 @@ def merkle_root(hashes): def little_endian_to_int(b): - '''little_endian_to_int takes byte sequence as a little-endian number. - Returns an integer''' + """little_endian_to_int takes byte sequence as a little-endian number. + Returns an integer""" # use the int.from_bytes(b, ) method - return int.from_bytes(b, 'little') + return int.from_bytes(b, "little") def int_to_little_endian(n, length): - '''endian_to_little_endian takes an integer and returns the little-endian - byte sequence of length''' + """endian_to_little_endian takes an integer and returns the little-endian + byte sequence of length""" # use the to_bytes method of n - return n.to_bytes(length, 'little') + return n.to_bytes(length, "little") def bytes_to_bit_field(some_bytes): @@ -93,10 +92,13 @@ def bytes_to_bit_field(some_bytes): byte >>= 1 return flag_bits + class Block: - command = b'block' + command = b"block" - def __init__(self, version, prev_block, merkle_root, timestamp, bits, nonce, tx_hashes=None): + def __init__( + self, version, prev_block, merkle_root, timestamp, bits, nonce, tx_hashes=None + ): self.version = version self.prev_block = prev_block self.merkle_root = merkle_root @@ -108,7 +110,7 @@ def __init__(self, version, prev_block, merkle_root, timestamp, bits, nonce, tx_ @classmethod def parse_header(cls, s): - '''Takes a byte stream and parses a block. Returns a Block object''' + """Takes a byte stream and parses a block. Returns a Block object""" # s.read(n) will read n bytes from the stream # version - 4 bytes, little endian, interpret as int version = little_endian_to_int(s.read(4)) @@ -137,7 +139,7 @@ def parse(cls, s): return b def serialize(self): - '''Returns the 80 byte block header''' + """Returns the 80 byte block header""" # version - 4 bytes, little endian result = int_to_little_endian(self.version, 4) # prev_block - 32 bytes, little endian @@ -153,7 +155,7 @@ def serialize(self): return result def hash(self): - '''Returns the hash256 interpreted little endian of the block''' + """Returns the hash256 interpreted little endian of the block""" # serialize s = self.serialize() # hash256 @@ -162,47 +164,47 @@ def hash(self): return h256[::-1] def id(self): - '''Human-readable hexadecimal of the block hash''' + """Human-readable hexadecimal of the block hash""" return self.hash().hex() def bip9(self): - '''Returns whether this block is signaling readiness for BIP9''' + """Returns whether this block is signaling readiness for BIP9""" # BIP9 is signalled if the top 3 bits are 001 # remember version is 32 bytes so right shift 29 (>> 29) and see if # that is 001 return self.version >> 29 == 0b001 def bip91(self): - '''Returns whether this block is signaling readiness for BIP91''' + """Returns whether this block is signaling readiness for BIP91""" # BIP91 is signalled if the 5th bit from the right is 1 # shift 4 bits to the right and see if the last bit is 1 return self.version >> 4 & 1 == 1 def bip141(self): - '''Returns whether this block is signaling readiness for BIP141''' + """Returns whether this block is signaling readiness for BIP141""" # BIP91 is signalled if the 2nd bit from the right is 1 # shift 1 bit to the right and see if the last bit is 1 return self.version >> 1 & 1 == 1 def target(self): - '''Returns the proof-of-work target based on the bits''' + """Returns the proof-of-work target based on the bits""" # last byte is exponent exponent = self.bits[-1] # the first three bytes are the coefficient in little endian coefficient = little_endian_to_int(self.bits[:-1]) # the formula is: # coefficient * 256**(exponent-3) - return coefficient * 256**(exponent - 3) + return coefficient * 256 ** (exponent - 3) def difficulty(self): - '''Returns the block difficulty based on the bits''' + """Returns the block difficulty based on the bits""" # note difficulty is (target of lowest difficulty) / (self's target) # lowest difficulty has bits that equal 0xffff001d - lowest = 0xffff * 256**(0x1d - 3) + lowest = 0xFFFF * 256 ** (0x1D - 3) return lowest / self.target() def check_pow(self): - '''Returns whether this block satisfies proof of work''' + """Returns whether this block satisfies proof of work""" # get the hash256 of the serialization of this block h256 = hash256(self.serialize()) # interpret this hash as a little-endian number @@ -211,9 +213,9 @@ def check_pow(self): return proof < self.target() def validate_merkle_root(self): - '''Gets the merkle root of the tx_hashes and checks that it's + """Gets the merkle root of the tx_hashes and checks that it's the same as the merkle root of this block. - ''' + """ # reverse all the transaction hashes (self.tx_hashes) hashes = [h[::-1] for h in self.tx_hashes] # get the Merkle Root @@ -224,10 +226,7 @@ def validate_merkle_root(self): return root[::-1] == self.merkle_root - - class MerkleTree: - def __init__(self, total): self.total = total # compute max depth math.ceil(math.log(self.total, 2)) @@ -238,7 +237,7 @@ def __init__(self, total): for depth in range(self.max_depth + 1): # the number of items at this depth is # math.ceil(self.total / 2**(self.max_depth - depth)) - num_items = math.ceil(self.total / 2**(self.max_depth - depth)) + num_items = math.ceil(self.total / 2 ** (self.max_depth - depth)) # create this level's hashes list with the right number of items level_hashes = [None] * num_items # append this level's hashes to the merkle tree @@ -254,15 +253,15 @@ def __repr__(self): items = [] for index, h in enumerate(level): if h is None: - short = 'None' + short = "None" else: - short = '{}...'.format(h.hex()[:8]) + short = "{}...".format(h.hex()[:8]) if depth == self.current_depth and index == self.current_index: - items.append('*{}*'.format(short[:-2])) + items.append("*{}*".format(short[:-2])) else: - items.append('{}'.format(short)) - result.append(', '.join(items)) - return '\n'.join(result) + items.append("{}".format(short)) + result.append(", ".join(items)) + return "\n".join(result) def up(self): # reduce depth by 1 and halve the index @@ -352,14 +351,14 @@ def populate_tree(self, flag_bits, hashes): # we've completed this sub-tree, go up self.up() if len(hashes) != 0: - raise RuntimeError('hashes not all consumed {}'.format(len(hashes))) + raise RuntimeError("hashes not all consumed {}".format(len(hashes))) for flag_bit in flag_bits: if flag_bit != 0: - raise RuntimeError('flag bits not all consumed') + raise RuntimeError("flag bits not all consumed") class MerkleBlock: - command = b'merkleblock' + command = b"merkleblock" def __init__(self, header, total, hashes, flags): self.header = header @@ -369,20 +368,20 @@ def __init__(self, header, total, hashes, flags): self.merkle_tree = None def __repr__(self): - result = '{}\n'.format(self.total) + result = "{}\n".format(self.total) for h in self.hashes: - result += '\t{}\n'.format(h.hex()) - result += '{}'.format(self.flags.hex()) + result += "\t{}\n".format(h.hex()) + result += "{}".format(self.flags.hex()) def hash(self): return self.header.hash() def id(self): return self.header.id() - + @classmethod def parse(cls, s): - '''Takes a byte stream and parses a merkle block. Returns a Merkle Block object''' + """Takes a byte stream and parses a merkle block. Returns a Merkle Block object""" # s.read(n) will read n bytes from the stream # header - use Block.parse_header with the stream header = Block.parse_header(s) @@ -404,7 +403,7 @@ def parse(cls, s): return cls(header, total, hashes, flags) def is_valid(self): - '''Verifies whether the merkle tree information validates to the merkle root''' + """Verifies whether the merkle tree information validates to the merkle root""" # use bytes_to_bit_field on self.flags to get the flag_bits flag_bits = bytes_to_bit_field(self.flags) # set hashes to be the reversed hashes of everything in self.hashes @@ -417,14 +416,16 @@ def is_valid(self): return self.merkle_tree.root()[::-1] == self.header.merkle_root def proved_txs(self): - '''Returns the list of proven transactions from the Merkle block''' + """Returns the list of proven transactions from the Merkle block""" if self.merkle_tree is None: return [] else: return self.merkle_tree.proved_txs -def is_valid_merkle_proof(proof_hex, target_tx_hex, target_block_hash_hex, target_merkle_root_hex=None): +def is_valid_merkle_proof( + proof_hex, target_tx_hex, target_block_hash_hex, target_merkle_root_hex=None +): """ Validate a `target_tx` and `target_block_hash` are part of a BIP37 merkle `proof` """ @@ -440,7 +441,7 @@ def is_valid_merkle_proof(proof_hex, target_tx_hex, target_block_hash_hex, targe if target_merkle_root_hex is not None: if mb.merkle_tree.root()[::-1].hex() != target_merkle_root_hex: return False - + if mb.hash().hex() != target_block_hash_hex: return False diff --git a/src/cryptoadvance/specter/util/tor.py b/src/cryptoadvance/specter/util/tor.py index adffe20d3f..da49b037a6 100644 --- a/src/cryptoadvance/specter/util/tor.py +++ b/src/cryptoadvance/specter/util/tor.py @@ -5,7 +5,7 @@ def start_hidden_service(app): app.controller.reconnect() key_path = os.path.abspath( - os.path.join(app.specter.data_folder, '.tor_service_key') + os.path.join(app.specter.data_folder, ".tor_service_key") ) app.tor_service_id = None @@ -15,17 +15,15 @@ def start_hidden_service(app): ) app.tor_service_id = service.service_id print( - '* Started a new hidden service with the address of %s.onion' + "* Started a new hidden service with the address of %s.onion" % app.tor_service_id ) - with open(key_path, 'w') as key_file: - key_file.write( - '%s:%s' % (service.private_key_type, service.private_key) - ) + with open(key_path, "w") as key_file: + key_file.write("%s:%s" % (service.private_key_type, service.private_key)) else: with open(key_path) as key_file: - key_type, key_content = key_file.read().split(':', 1) + key_type, key_content = key_file.read().split(":", 1) service = app.controller.create_ephemeral_hidden_service( {app.tor_port: app.port}, @@ -34,12 +32,12 @@ def start_hidden_service(app): await_publication=True, ) app.tor_service_id = service.service_id - print('* Resumed %s.onion' % app.tor_service_id) + print("* Resumed %s.onion" % app.tor_service_id) # save address to file if app.save_tor_address_to is not None: - with open(app.save_tor_address_to, 'w') as f: - f.write('%s.onion' % app.tor_service_id) + with open(app.save_tor_address_to, "w") as f: + f.write("%s.onion" % app.tor_service_id) app.tor_service_id = app.tor_service_id app.tor_enabled = True @@ -48,14 +46,14 @@ def stop_hidden_services(app): try: app.controller.reconnect() hidden_services = app.controller.list_ephemeral_hidden_services() - print(' * Shutting down our hidden service') + print(" * Shutting down our hidden service") for tor_service_id in hidden_services: app.controller.remove_ephemeral_hidden_service(tor_service_id) # Sanity - if (len(app.controller.list_ephemeral_hidden_services()) != 0): - print(' * Failed to shut down our hidden services...') + if len(app.controller.list_ephemeral_hidden_services()) != 0: + print(" * Failed to shut down our hidden services...") else: - print(' * Hidden services were shut down successfully') + print(" * Hidden services were shut down successfully") app.tor_service_id = None except Exception: pass # we tried... diff --git a/src/cryptoadvance/specter/util/xpub.py b/src/cryptoadvance/specter/util/xpub.py index a1265cffc6..10e12b3171 100644 --- a/src/cryptoadvance/specter/util/xpub.py +++ b/src/cryptoadvance/specter/util/xpub.py @@ -1,14 +1,17 @@ import hashlib from .base58 import decode_base58, encode_base58_checksum + def hash160(d): - return hashlib.new('ripemd160', hashlib.sha256(d).digest()).digest() + return hashlib.new("ripemd160", hashlib.sha256(d).digest()).digest() + def convert_xpub_prefix(xpub, prefix_bytes): # Update xpub to specified prefix and re-encode b = decode_base58(xpub) return encode_base58_checksum(prefix_bytes + b[4:]) + def get_xpub_fingerprint(xpub): """ Retuns fingerprint of the XPUB itself. diff --git a/src/cryptoadvance/specter/wallet.py b/src/cryptoadvance/specter/wallet.py index a500df32d4..4c8eccb4c8 100644 --- a/src/cryptoadvance/specter/wallet.py +++ b/src/cryptoadvance/specter/wallet.py @@ -17,11 +17,12 @@ logger = logging.getLogger() -class Wallet(): +class Wallet: # if the wallet is old we import 300 addresses IMPORT_KEYPOOL = 300 # a gap of 20 addresses is what many wallets do GAP_LIMIT = 20 + def __init__( self, name, @@ -68,9 +69,7 @@ def __init__( for device in devices ] if None in self.devices: - raise Exception( - 'A device used by this wallet could not have been found!' - ) + raise Exception("A device used by this wallet could not have been found!") self.sigs_required = sigs_required self.pending_psbts = pending_psbts self.fullpath = fullpath @@ -80,9 +79,9 @@ def __init__( ) self.last_block = last_block - if address == '': + if address == "": self.getnewaddress() - if change_address == '': + if change_address == "": self.getnewaddress(change=True) self.getdata() @@ -113,14 +112,14 @@ def check_addresses(self): addresses = list(dict.fromkeys(addresses)) if len(addresses) > 0: # prepare rpc call - calls = [("getaddressinfo",addr) for addr in addresses] + calls = [("getaddressinfo", addr) for addr in addresses] # extract results res = [r["result"] for r in self.rpc.multi(calls)] # extract last two indexes of hdkeypath paths = [d["hdkeypath"].split("/")[-2:] for d in res if "hdkeypath" in d] # get change and recv addresses - max_recv = max([int(p[1]) for p in paths if p[0]=="0"], default=-1) - max_change = max([int(p[1]) for p in paths if p[0]=="1"], default=-1) + max_recv = max([int(p[1]) for p in paths if p[0] == "0"], default=-1) + max_change = max([int(p[1]) for p in paths if p[0] == "1"], default=-1) # these calls will happen only if current addresses are used updated = False while max_recv >= self.address_index: @@ -139,57 +138,89 @@ def parse_old_format(wallet_dict, device_manager): old_format_detected = False new_dict = {} new_dict.update(wallet_dict) - if 'key' in wallet_dict: - new_dict['keys'] = [wallet_dict['key']] - del new_dict['key'] + if "key" in wallet_dict: + new_dict["keys"] = [wallet_dict["key"]] + del new_dict["key"] old_format_detected = True - if 'device' in wallet_dict: - new_dict['devices'] = [wallet_dict['device']] - del new_dict['device'] + if "device" in wallet_dict: + new_dict["devices"] = [wallet_dict["device"]] + del new_dict["device"] old_format_detected = True - devices = [device_manager.get_by_alias(device) for device in new_dict['devices']] - if len(new_dict['keys']) > 1 and 'sortedmulti' not in new_dict['recv_descriptor']: - new_dict['recv_descriptor'] = AddChecksum(new_dict['recv_descriptor'].replace('multi', 'sortedmulti').split('#')[0]) + devices = [ + device_manager.get_by_alias(device) for device in new_dict["devices"] + ] + if ( + len(new_dict["keys"]) > 1 + and "sortedmulti" not in new_dict["recv_descriptor"] + ): + new_dict["recv_descriptor"] = AddChecksum( + new_dict["recv_descriptor"] + .replace("multi", "sortedmulti") + .split("#")[0] + ) old_format_detected = True - if len(new_dict['keys']) > 1 and 'sortedmulti' not in new_dict['change_descriptor']: - new_dict['change_descriptor'] = AddChecksum(new_dict['change_descriptor'].replace('multi', 'sortedmulti').split('#')[0]) + if ( + len(new_dict["keys"]) > 1 + and "sortedmulti" not in new_dict["change_descriptor"] + ): + new_dict["change_descriptor"] = AddChecksum( + new_dict["change_descriptor"] + .replace("multi", "sortedmulti") + .split("#")[0] + ) old_format_detected = True if None in devices: - devices = [((device['name'] if isinstance(device, dict) else device) if (device['name'] if isinstance(device, dict) else device) in device_manager.devices else None) for device in new_dict['devices']] + devices = [ + ( + (device["name"] if isinstance(device, dict) else device) + if (device["name"] if isinstance(device, dict) else device) + in device_manager.devices + else None + ) + for device in new_dict["devices"] + ] if None in devices: - raise Exception('A device used by this wallet could not have been found!') + raise Exception( + "A device used by this wallet could not have been found!" + ) else: - new_dict['devices'] = [device_manager.devices[device].alias for device in devices] + new_dict["devices"] = [ + device_manager.devices[device].alias for device in devices + ] old_format_detected = True - new_dict['old_format_detected'] = old_format_detected + new_dict["old_format_detected"] = old_format_detected return new_dict @classmethod - def from_json(cls, wallet_dict, device_manager, manager, default_alias='', default_fullpath=''): - name = wallet_dict.get('name', '') - alias = wallet_dict.get('alias', default_alias) - description = wallet_dict.get('description', '') - address = wallet_dict.get('address', '') - address_index = wallet_dict.get('address_index', 0) - change_address = wallet_dict.get('change_address', '') - change_index = wallet_dict.get('change_index', 0) - keypool = wallet_dict.get('keypool', 0) - change_keypool = wallet_dict.get('change_keypool', 0) - sigs_required = wallet_dict.get('sigs_required', 1) - pending_psbts = wallet_dict.get('pending_psbts', {}) - fullpath = wallet_dict.get('fullpath', default_fullpath) - last_block = wallet_dict.get('last_block', None) + def from_json( + cls, wallet_dict, device_manager, manager, default_alias="", default_fullpath="" + ): + name = wallet_dict.get("name", "") + alias = wallet_dict.get("alias", default_alias) + description = wallet_dict.get("description", "") + address = wallet_dict.get("address", "") + address_index = wallet_dict.get("address_index", 0) + change_address = wallet_dict.get("change_address", "") + change_index = wallet_dict.get("change_index", 0) + keypool = wallet_dict.get("keypool", 0) + change_keypool = wallet_dict.get("change_keypool", 0) + sigs_required = wallet_dict.get("sigs_required", 1) + pending_psbts = wallet_dict.get("pending_psbts", {}) + fullpath = wallet_dict.get("fullpath", default_fullpath) + last_block = wallet_dict.get("last_block", None) wallet_dict = Wallet.parse_old_format(wallet_dict, device_manager) try: - address_type = wallet_dict['address_type'] - recv_descriptor = wallet_dict['recv_descriptor'] - change_descriptor = wallet_dict['change_descriptor'] - keys = [Key.from_json(key_dict) for key_dict in wallet_dict['keys']] - devices = wallet_dict['devices'] + address_type = wallet_dict["address_type"] + recv_descriptor = wallet_dict["recv_descriptor"] + change_descriptor = wallet_dict["change_descriptor"] + keys = [Key.from_json(key_dict) for key_dict in wallet_dict["keys"]] + devices = wallet_dict["devices"] except: - raise Exception('Could not construct a Wallet object from the data provided.') + raise Exception( + "Could not construct a Wallet object from the data provided." + ) return cls( name, @@ -211,8 +242,8 @@ def from_json(cls, wallet_dict, device_manager, manager, default_alias='', defau fullpath, device_manager, manager, - old_format_detected=wallet_dict['old_format_detected'], - last_block=last_block + old_format_detected=wallet_dict["old_format_detected"], + last_block=last_block, ) def get_info(self): @@ -231,14 +262,13 @@ def getdata(self): # TODO: Should do the same for the non change address (?) # check if address was used already try: - value_on_address = self.rpc.getreceivedbyaddress( - self.change_address, - 0 - ) + value_on_address = self.rpc.getreceivedbyaddress(self.change_address, 0) except: # Could happen if address not in wallet (wallet was imported) # try adding keypool - logger.info(f"Didn't get transactions on address {self.change_address}. Refilling keypool.") + logger.info( + f"Didn't get transactions on address {self.change_address}. Refilling keypool." + ) self.keypoolrefill(0, end=self.keypool, change=False) self.keypoolrefill(0, end=self.change_keypool, change=True) value_on_address = 0 @@ -269,7 +299,7 @@ def json(self): "pending_psbts": self.pending_psbts, "fullpath": self.fullpath, "last_block": self.last_block, - "blockheight": self.blockheight + "blockheight": self.blockheight, } def save_to_file(self): @@ -286,7 +316,12 @@ def is_multisig(self): def locked_amount(self): amount = 0 for psbt in self.pending_psbts: - amount += sum([utxo["witness_utxo"]["amount"] for utxo in self.pending_psbts[psbt]["inputs"]]) + amount += sum( + [ + utxo["witness_utxo"]["amount"] + for utxo in self.pending_psbts[psbt]["inputs"] + ] + ) return amount def delete_pending_psbt(self, txid): @@ -304,7 +339,9 @@ def update_pending_psbt(self, psbt, txid, raw): self.pending_psbts[txid]["base64"] = psbt decodedpsbt = self.rpc.decodepsbt(psbt) signed_devices = self.get_signed_devices(decodedpsbt) - self.pending_psbts[txid]["devices_signed"] = [dev.name for dev in signed_devices] + self.pending_psbts[txid]["devices_signed"] = [ + dev.name for dev in signed_devices + ] if "hex" in raw: self.pending_psbts[txid]["sigs_count"] = self.sigs_required self.pending_psbts[txid]["raw"] = raw["hex"] @@ -322,7 +359,9 @@ def save_pending_psbt(self, psbt): def txlist(self, idx, wallet_tx_batch=100, validate_merkle_proofs=False): try: - rpc_txs = self.rpc.listtransactions("*", wallet_tx_batch + 2, wallet_tx_batch * idx, True) # get batch + 2 to make sure you have information about send + rpc_txs = self.rpc.listtransactions( + "*", wallet_tx_batch + 2, wallet_tx_batch * idx, True + ) # get batch + 2 to make sure you have information about send rpc_txs.reverse() transactions = rpc_txs[:wallet_tx_batch] except: @@ -330,27 +369,47 @@ def txlist(self, idx, wallet_tx_batch=100, validate_merkle_proofs=False): result = [] blocks = {} for tx in transactions: - if 'confirmations' not in tx: - tx['confirmations'] = 0 - if len([_tx for _tx in rpc_txs if (_tx['txid'] == tx['txid'] and _tx['address'] == tx['address'])]) > 1: - continue # means the tx is duplicated (change), continue - - tx['validated_blockhash'] = "" # default is assume unvalidated - if validate_merkle_proofs is True and tx['confirmations'] > 0 and tx.get('blockhash'): - proof_hex = self.rpc.gettxoutproof([tx['txid']], tx['blockhash']) - logger.debug(f"Attempting merkle proof validation of tx { tx['txid'] } in block { tx['blockhash'] }") + if "confirmations" not in tx: + tx["confirmations"] = 0 + if ( + len( + [ + _tx + for _tx in rpc_txs + if ( + _tx["txid"] == tx["txid"] + and _tx["address"] == tx["address"] + ) + ] + ) + > 1 + ): + continue # means the tx is duplicated (change), continue + + tx["validated_blockhash"] = "" # default is assume unvalidated + if ( + validate_merkle_proofs is True + and tx["confirmations"] > 0 + and tx.get("blockhash") + ): + proof_hex = self.rpc.gettxoutproof([tx["txid"]], tx["blockhash"]) + logger.debug( + f"Attempting merkle proof validation of tx { tx['txid'] } in block { tx['blockhash'] }" + ) if is_valid_merkle_proof( proof_hex=proof_hex, - target_tx_hex=tx['txid'], - target_block_hash_hex=tx['blockhash'], + target_tx_hex=tx["txid"], + target_block_hash_hex=tx["blockhash"], target_merkle_root_hex=None, ): # NOTE: this does NOT guarantee this blockhash is actually in the real Bitcoin blockchain! # See merkletooltip.html for details logger.debug(f"Merkle proof of { tx['txid'] } validation success") - tx['validated_blockhash'] = tx['blockhash'] + tx["validated_blockhash"] = tx["blockhash"] else: - logger.warning(f"Attempted merkle proof validation on {tx['txid']} but failed. This is likely a configuration error but perhaps your node is compromised! Details: {proof_hex}") + logger.warning( + f"Attempted merkle proof validation on {tx['txid']} but failed. This is likely a configuration error but perhaps your node is compromised! Details: {proof_hex}" + ) result.append(tx) @@ -366,23 +425,27 @@ def _rescan_utxo_thread(self, explorer=None): # and adjust keypool accordingly args = [ "start", - [{ - "desc": self.recv_descriptor, - "range": max(self.keypool, 1000) - },{ - "desc": self.change_descriptor, - "range": max(self.change_keypool, 1000) - }] + [ + {"desc": self.recv_descriptor, "range": max(self.keypool, 1000)}, + { + "desc": self.change_descriptor, + "range": max(self.change_keypool, 1000), + }, + ], ] unspents = self.rpc.scantxoutset(*args)["unspents"] # if keypool adjustments fails - not a big deal try: # check derivation indexes in found unspents (last 2 indexes in [brackets]) - derivations = [tx["desc"].split("[")[1].split("]")[0].split("/")[-2:] - for tx in unspents] + derivations = [ + tx["desc"].split("[")[1].split("]")[0].split("/")[-2:] + for tx in unspents + ] # get max derivation for change and receive branches - max_recv = max([-1]+[int(der[1]) for der in derivations if der[0] == '0']) - max_change = max([-1]+[int(der[1]) for der in derivations if der[0] == '1']) + max_recv = max([-1] + [int(der[1]) for der in derivations if der[0] == "0"]) + max_change = max( + [-1] + [int(der[1]) for der in derivations if der[0] == "1"] + ) updated = False if max_recv >= self.address_index: @@ -404,40 +467,37 @@ def _rescan_utxo_thread(self, explorer=None): logger.warning(f"Failed to get derivation path from utxo transaction: {e}") # keep working with unspents - res = self.rpc.multi([ - ("getblockhash", tx["height"]) - for tx in unspents - ]) + res = self.rpc.multi([("getblockhash", tx["height"]) for tx in unspents]) block_hashes = [r["result"] for r in res] for i, tx in enumerate(unspents): tx["blockhash"] = block_hashes[i] - res = self.rpc.multi([ - ("gettxoutproof", [tx["txid"]], tx["blockhash"]) - for tx in unspents - ]) + res = self.rpc.multi( + [("gettxoutproof", [tx["txid"]], tx["blockhash"]) for tx in unspents] + ) proofs = [r["result"] for r in res] for i, tx in enumerate(unspents): tx["proof"] = proofs[i] - res = self.rpc.multi([ - ("getrawtransaction", tx["txid"], False, tx["blockhash"]) - for tx in unspents - ]) + res = self.rpc.multi( + [ + ("getrawtransaction", tx["txid"], False, tx["blockhash"]) + for tx in unspents + ] + ) raws = [r["result"] for r in res] for i, tx in enumerate(unspents): tx["raw"] = raws[i] missing = [tx for tx in unspents if tx["raw"] is None] existing = [tx for tx in unspents if tx["raw"] is not None] - self.rpc.multi([ - ("importprunedfunds", tx["raw"], tx["proof"]) - for tx in existing - ]) + self.rpc.multi( + [("importprunedfunds", tx["raw"], tx["proof"]) for tx in existing] + ) # handle missing transactions now # if Tor is running, requests will be sent over Tor if explorer is not None: try: requests_session = requests.Session() - requests_session.proxies['http'] = 'socks5h://localhost:9050' - requests_session.proxies['https'] = 'socks5h://localhost:9050' + requests_session.proxies["http"] = "socks5h://localhost:9050" + requests_session.proxies["https"] = "socks5h://localhost:9050" requests_session.get(explorer) except Exception: requests_session = requests.Session() @@ -445,27 +505,31 @@ def _rescan_utxo_thread(self, explorer=None): explorer = explorer.rstrip("/") try: # get raw transactions - raws = [requests_session.get( - f"{explorer}/api/tx/{tx['txid']}/hex" - ).text - for tx in missing] + raws = [ + requests_session.get(f"{explorer}/api/tx/{tx['txid']}/hex").text + for tx in missing + ] # get proofs - proofs = [requests_session.get( - f"{explorer}/api/tx/{tx['txid']}/merkleblock-proof" - ).text - for tx in missing] + proofs = [ + requests_session.get( + f"{explorer}/api/tx/{tx['txid']}/merkleblock-proof" + ).text + for tx in missing + ] # import funds - self.rpc.multi([ - ("importprunedfunds", raws[i], proofs[i]) - for i in range(len(raws)) - ]) + self.rpc.multi( + [ + ("importprunedfunds", raws[i], proofs[i]) + for i in range(len(raws)) + ] + ) except Exception as e: logger.warning(f"Failed to fetch data from block explorer: {e}") @property def rescan_progress(self): """Returns None if rescanblockchain is not launched, - value between 0 and 1 otherwise + value between 0 and 1 otherwise """ if "scanning" not in self.info or self.info["scanning"] == False: return None @@ -476,22 +540,34 @@ def rescan_progress(self): def blockheight(self): txs = self.rpc.listtransactions("*", 100, 0, True) i = 0 - while (len(txs) == 100): + while len(txs) == 100: i += 1 next_txs = self.rpc.listtransactions("*", 100, i * 100, True) - if (len(next_txs) > 0): + if len(next_txs) > 0: txs = next_txs else: break current_blockheight = self.rpc.getblockcount() - if len(txs) > 0 and 'confirmations' in txs[0]: - blockheight = current_blockheight - txs[0]['confirmations'] - 101 # To ensure coinbase transactions are indexed properly - return 0 if blockheight < 0 else blockheight # To ensure regtest don't have negative blockheight + if len(txs) > 0 and "confirmations" in txs[0]: + blockheight = ( + current_blockheight - txs[0]["confirmations"] - 101 + ) # To ensure coinbase transactions are indexed properly + return ( + 0 if blockheight < 0 else blockheight + ) # To ensure regtest don't have negative blockheight return current_blockheight @property def account_map(self): - return '{ "label": "' + self.name.replace("'","\\'") + '", "blockheight": ' + str(self.blockheight) + ', "descriptor": "' + self.recv_descriptor.replace("/", "\\/") + '" }' + return ( + '{ "label": "' + + self.name.replace("'", "\\'") + + '", "blockheight": ' + + str(self.blockheight) + + ', "descriptor": "' + + self.recv_descriptor.replace("/", "\\/") + + '" }' + ) def getnewaddress(self, change=False, save=True): label = "Change" if change else "Address" @@ -519,15 +595,10 @@ def get_address(self, index, change=False): if self.is_multisig: try: # first try with sortedmulti - addr = self.rpc.deriveaddresses(desc, [index, index+1])[0] + addr = self.rpc.deriveaddresses(desc, [index, index + 1])[0] except Exception: # if sortedmulti is not supported - desc = sort_descriptor( - self.rpc, - desc, - index=index, - change=change - ) + desc = sort_descriptor(self.rpc, desc, index=index, change=change) addr = self.rpc.deriveaddresses(desc)[0] return addr return self.rpc.deriveaddresses(desc, [index, index + 1])[0] @@ -536,7 +607,7 @@ def get_balance(self): try: self.balance = self.rpc.getbalances()["watchonly"] except: - self.balance = { "trusted": 0, "untrusted_pending": 0 } + self.balance = {"trusted": 0, "untrusted_pending": 0} return self.balance def keypoolrefill(self, start, end=None, change=False): @@ -546,11 +617,11 @@ def keypoolrefill(self, start, end=None, change=False): args = [ { "desc": desc, - "internal": change, - "range": [start, end], - "timestamp": "now", - "keypool": True, - "watchonly": True + "internal": change, + "range": [start, end], + "timestamp": "now", + "keypool": True, + "watchonly": True, } ] if not self.is_multisig: @@ -560,7 +631,7 @@ def keypoolrefill(self, start, end=None, change=False): # try if sortedmulti is supported r = self.rpc.importmulti(args, {"rescan": False}) # doesn't raise, but instead returns "success": False - if not r[0]['success']: + if not r[0]["success"]: # first import normal multi # remove checksum desc = desc.split("#")[0] @@ -578,10 +649,7 @@ def keypoolrefill(self, start, end=None, change=False): batch = [] for i in range(start, end): sorted_desc = sort_descriptor( - self.rpc, - desc, - index=i, - change=change + self.rpc, desc, index=i, change=change ) # create fresh object obj = {} @@ -601,7 +669,9 @@ def utxo_on_address(self, address): return len(utxo) def balance_on_address(self, address): - balancelist = [utxo["amount"] for utxo in self.utxo if utxo["address"] == address] + balancelist = [ + utxo["amount"] for utxo in self.utxo if utxo["address"] == address + ] return sum(balancelist) def utxo_on_label(self, label): @@ -609,13 +679,23 @@ def utxo_on_label(self, label): return len(utxo) def balance_on_label(self, label): - balancelist = [utxo["amount"] for utxo in self.utxo if self.getlabel(utxo["address"]) == label] + balancelist = [ + utxo["amount"] + for utxo in self.utxo + if self.getlabel(utxo["address"]) == label + ] return sum(balancelist) def addresses_on_label(self, label): - return list(dict.fromkeys( - [address for address in (self.addresses + self.change_addresses) if self.getlabel(address) == label] - )) + return list( + dict.fromkeys( + [ + address + for address in (self.addresses + self.change_addresses) + if self.getlabel(address) == label + ] + ) + ) @property def is_current_address_used(self): @@ -623,11 +703,25 @@ def is_current_address_used(self): @property def utxo_addresses(self): - return list(dict.fromkeys([utxo["address"] for utxo in sorted(self.utxo, key = lambda utxo: utxo["time"])])) + return list( + dict.fromkeys( + [ + utxo["address"] + for utxo in sorted(self.utxo, key=lambda utxo: utxo["time"]) + ] + ) + ) @property def utxo_labels(self): - return list(dict.fromkeys([self.getlabel(utxo["address"]) for utxo in sorted(self.utxo, key = lambda utxo: utxo["time"])])) + return list( + dict.fromkeys( + [ + self.getlabel(utxo["address"]) + for utxo in sorted(self.utxo, key=lambda utxo: utxo["time"]) + ] + ) + ) def setlabel(self, address, label): self.rpc.setlabel(address, label) @@ -635,14 +729,25 @@ def setlabel(self, address, label): def getlabel(self, address): address_info = self.rpc.getaddressinfo(address) # Bitcoin Core version 0.20.0 has replaced the `label` field with `labels`, an array currently limited to a single item. - label = address_info["labels"][0] if ( - "labels" in address_info - and (isinstance(address_info["labels"], list) - and len(address_info["labels"]) > 0) - and "label" not in address_info) else address + label = ( + address_info["labels"][0] + if ( + "labels" in address_info + and ( + isinstance(address_info["labels"], list) + and len(address_info["labels"]) > 0 + ) + and "label" not in address_info + ) + else address + ) if label == "": label = address - return address_info["label"] if "label" in address_info and address_info["label"] != "" else label + return ( + address_info["label"] + if "label" in address_info and address_info["label"] != "" + else label + ) def get_address_name(self, address, addr_idx): if self.getlabel(address) == address and addr_idx > -1: @@ -684,7 +789,10 @@ def active_addresses(self): @property def change_addresses(self): - return [self.get_address(idx, change=True) for idx in range(0, self.change_index + 1)] + return [ + self.get_address(idx, change=True) + for idx in range(0, self.change_index + 1) + ] @property def wallet_addresses(self): @@ -692,15 +800,28 @@ def wallet_addresses(self): @property def labels(self): - return list(dict.fromkeys([self.getlabel(addr) for addr in self.active_addresses])) + return list( + dict.fromkeys([self.getlabel(addr) for addr in self.active_addresses]) + ) - def createpsbt(self, addresses:[str], amounts:[float], subtract:bool=False, subtract_from:int=0, fee_rate:float=1.0, selected_coins=[], readonly=False): + def createpsbt( + self, + addresses: [str], + amounts: [float], + subtract: bool = False, + subtract_from: int = 0, + fee_rate: float = 1.0, + selected_coins=[], + readonly=False, + ): """ - fee_rate: in sat/B or BTC/kB. Default (None) bitcoin core sets feeRate automatically. + fee_rate: in sat/B or BTC/kB. Default (None) bitcoin core sets feeRate automatically. """ if self.full_available_balance < sum(amounts): - raise SpecterError('The wallet does not have sufficient funds to make the transaction.') + raise SpecterError( + "The wallet does not have sufficient funds to make the transaction." + ) extra_inputs = [] if self.available_balance["trusted"] < sum(amounts): @@ -722,7 +843,9 @@ def createpsbt(self, addresses:[str], amounts:[float], subtract:bool=False, subt if still_needed < 0: break if still_needed > 0: - raise SpecterError("Selected coins does not cover Full amount! Please select more coins!") + raise SpecterError( + "Selected coins does not cover Full amount! Please select more coins!" + ) # subtract fee from amount of this output: # currently only one address is supported, so either @@ -730,9 +853,9 @@ def createpsbt(self, addresses:[str], amounts:[float], subtract:bool=False, subt subtract_arr = [subtract_from] if subtract else [] options = { - "includeWatching": True, + "includeWatching": True, "changeAddress": self.change_address, - "subtractFeeFromOutputs": subtract_arr + "subtractFeeFromOutputs": subtract_arr, } self.setlabel(self.change_address, "Change #{}".format(self.change_index)) @@ -742,33 +865,37 @@ def createpsbt(self, addresses:[str], amounts:[float], subtract:bool=False, subt # don't reuse change addresses - use getrawchangeaddress instead r = self.rpc.walletcreatefundedpsbt( - extra_inputs, # inputs - [{addresses[i]: amounts[i]} for i in range(len(addresses))], # output - 0, # locktime - options, # options - True # bip32-der + extra_inputs, # inputs + [{addresses[i]: amounts[i]} for i in range(len(addresses))], # output + 0, # locktime + options, # options + True, # bip32-der ) b64psbt = r["psbt"] psbt = self.rpc.decodepsbt(b64psbt) if fee_rate > 0.0: - psbt_fees_sats = int(psbt['fee'] * 1e8) - tx_full_size = psbt['tx']['vsize'] - for _ in psbt['inputs']: + psbt_fees_sats = int(psbt["fee"] * 1e8) + tx_full_size = psbt["tx"]["vsize"] + for _ in psbt["inputs"]: # size is weight / 4 - tx_full_size += self.weight_per_input/4 + tx_full_size += self.weight_per_input / 4 tx_full_size = ceil(tx_full_size) - adjusted_fee_rate = fee_rate * ( - fee_rate / (psbt_fees_sats / psbt['tx']['vsize']) - ) * (tx_full_size / psbt['tx']['vsize']) + adjusted_fee_rate = ( + fee_rate + * (fee_rate / (psbt_fees_sats / psbt["tx"]["vsize"])) + * (tx_full_size / psbt["tx"]["vsize"]) + ) # add 0.5 to make sure we round up - options["feeRate"] = '%.8f' % round((adjusted_fee_rate * 1000 + 0.5) / 1e8, 8) + options["feeRate"] = "%.8f" % round( + (adjusted_fee_rate * 1000 + 0.5) / 1e8, 8 + ) r = self.rpc.walletcreatefundedpsbt( - extra_inputs, # inputs - [{addresses[i]: amounts[i]} for i in range(len(addresses))], # output - 0, # locktime - options, # options - True # bip32-der + extra_inputs, # inputs + [{addresses[i]: amounts[i]} for i in range(len(addresses))], # output + 0, # locktime + options, # options + True, # bip32-der ) b64psbt = r["psbt"] @@ -776,7 +903,7 @@ def createpsbt(self, addresses:[str], amounts:[float], subtract:bool=False, subt psbt["tx_full_size"] = tx_full_size psbt["fee_rate"] = options["feeRate"] - psbt['base64'] = b64psbt + psbt["base64"] = b64psbt psbt["amount"] = amounts psbt["address"] = addresses psbt["time"] = time.time() @@ -786,12 +913,12 @@ def createpsbt(self, addresses:[str], amounts:[float], subtract:bool=False, subt return psbt - def fill_psbt(self, b64psbt, non_witness:bool=True, xpubs:bool=True): + def fill_psbt(self, b64psbt, non_witness: bool = True, xpubs: bool = True): psbt = PSBT() psbt.deserialize(b64psbt) if non_witness: for i, inp in enumerate(psbt.tx.vin): - txid = inp.prevout.hash.to_bytes(32,'big').hex() + txid = inp.prevout.hash.to_bytes(32, "big").hex() try: res = self.rpc.gettransaction(txid) except: @@ -810,15 +937,15 @@ def fill_psbt(self, b64psbt, non_witness:bool=True, xpubs:bool=True): # for multisig add xpub fields if len(self.keys) > 1: for k in self.keys: - key = b'\x01' + decode_base58(k.xpub) - if k.fingerprint != '': + key = b"\x01" + decode_base58(k.xpub) + if k.fingerprint != "": fingerprint = bytes.fromhex(k.fingerprint) else: fingerprint = get_xpub_fingerprint(k.xpub) - if k.derivation != '': + if k.derivation != "": der = der_to_bytes(k.derivation) else: - der = b'' + der = b"" value = fingerprint + der psbt.unknown[key] = value return psbt.serialize() @@ -848,12 +975,15 @@ def get_signed_devices(self, decodedpsbt): def importpsbt(self, b64psbt): # TODO: check maybe some of the inputs are already locked psbt = self.rpc.decodepsbt(b64psbt) - psbt['base64'] = b64psbt + psbt["base64"] = b64psbt amount = [] address = [] # get output address and amount for out in psbt["tx"]["vout"]: - if "addresses" not in out["scriptPubKey"] or len(out["scriptPubKey"]["addresses"]) == 0: + if ( + "addresses" not in out["scriptPubKey"] + or len(out["scriptPubKey"]["addresses"]) == 0 + ): # TODO: we need to handle it somehow differently raise SpecterError("Sending to raw scripts is not supported yet") addr = out["scriptPubKey"]["addresses"][0] @@ -880,19 +1010,19 @@ def importpsbt(self, b64psbt): def weight_per_input(self): """Calculates the weight of a signed input""" if self.is_multisig: - input_size = 3 # OP_M OP_N ... OP_CHECKMULTISIG + input_size = 3 # OP_M OP_N ... OP_CHECKMULTISIG for i in range(0, len(self.keys)): # pubkey size input_size += 34 for i in range(0, self.sigs_required): - input_size += 75 # max sig size + input_size += 75 # max sig size - if not self.recv_descriptor.startswith('wsh'): + if not self.recv_descriptor.startswith("wsh"): # P2SH scriptsig: 00 20 <32-byte-hash> input_size += 34 * 4 return input_size # else: single-sig - if self.recv_descriptor.startswith('wpkh'): + if self.recv_descriptor.startswith("wpkh"): # pubkey, signature return 75 + 34 # pubkey, signature, 4* P2SH: 00 14 20-byte-hash diff --git a/src/cryptoadvance/specter/wallet_manager.py b/src/cryptoadvance/specter/wallet_manager.py index f9c1ba0725..f67438ad2b 100644 --- a/src/cryptoadvance/specter/wallet_manager.py +++ b/src/cryptoadvance/specter/wallet_manager.py @@ -10,15 +10,17 @@ logger = logging.getLogger() -purposes = OrderedDict({ - None: "General", - "wpkh": "Single (Segwit)", - "sh-wpkh": "Single (Nested)", - "pkh": "Single (Legacy)", - "wsh": "Multisig (Segwit)", - "sh-wsh": "Multisig (Nested)", - "sh": "Multisig (Legacy)", -}) +purposes = OrderedDict( + { + None: "General", + "wpkh": "Single (Segwit)", + "sh-wpkh": "Single (Nested)", + "pkh": "Single (Legacy)", + "wsh": "Multisig (Segwit)", + "sh-wsh": "Multisig (Nested)", + "sh": "Multisig (Legacy)", + } +) addrtypes = { "pkh": "legacy", @@ -26,19 +28,13 @@ "wpkh": "bech32", "sh": "legacy", "sh-wsh": "p2sh-segwit", - "wsh": "bech32" + "wsh": "bech32", } + class WalletManager: # chain is required to manage wallets when bitcoind is not running - def __init__( - self, - data_folder, - rpc, - chain, - device_manager, - path="specter" - ): + def __init__(self, data_folder, rpc, chain, device_manager, path="specter"): self.data_folder = data_folder self.chain = chain self.rpc = rpc @@ -64,9 +60,7 @@ def update(self, data_folder=None, rpc=None, chain=None): self.working_folder = None if self.chain is not None and self.data_folder is not None: self.working_folder = os.path.join(self.data_folder, self.chain) - if self.working_folder is not None and not os.path.isdir( - self.working_folder - ): + if self.working_folder is not None and not os.path.isdir(self.working_folder): os.mkdir(self.working_folder) if rpc is not None: self.rpc = rpc @@ -89,59 +83,53 @@ def update(self, data_folder=None, rpc=None, chain=None): for wallet in wallets_files: wallet_alias = wallets_files[wallet]["alias"] wallet_name = wallets_files[wallet]["name"] - if existing_wallets is None or os.path.join( - self.rpc_path, - wallet_alias - ) in existing_wallets: - if os.path.join( - self.rpc_path, - wallet_alias - ) not in loaded_wallets: + if ( + existing_wallets is None + or os.path.join(self.rpc_path, wallet_alias) in existing_wallets + ): + if ( + os.path.join(self.rpc_path, wallet_alias) + not in loaded_wallets + ): try: logger.debug( - "loading %s " % - wallets_files[wallet]["alias"] + "loading %s " % wallets_files[wallet]["alias"] ) self.rpc.loadwallet( os.path.join(self.rpc_path, wallet_alias) ) wallets[wallet_name] = Wallet.from_json( - wallets_files[wallet], - self.device_manager, - self + wallets_files[wallet], self.device_manager, self ) # Lock UTXO of pending PSBTs if len(wallets[wallet_name].pending_psbts) > 0: - for psbt in wallets[ - wallet_name - ].pending_psbts: + for psbt in wallets[wallet_name].pending_psbts: logger.debug( - "lock %s " % - wallet_alias, - wallets[ - wallet_name - ].pending_psbts[psbt]["tx"]["vin"] + "lock %s " % wallet_alias, + wallets[wallet_name].pending_psbts[psbt][ + "tx" + ]["vin"], ) wallets[wallet_name].rpc.lockunspent( False, - [utxo for utxo in wallets[ - wallet_name - ].pending_psbts[ - psbt - ]["tx"]["vin"]] + [ + utxo + for utxo in wallets[ + wallet_name + ].pending_psbts[psbt]["tx"]["vin"] + ], ) except RpcError as e: logger.warn( f"Couldn't load wallet {wallet_alias} into core.\ -Silently ignored! RPC error: {e}") +Silently ignored! RPC error: {e}" + ) else: if wallet_name not in existing_names: # ok wallet is already there # we only need to update wallets[wallet_name] = Wallet.from_json( - wallets_files[wallet], - self.device_manager, - self + wallets_files[wallet], self.device_manager, self ) else: # wallet is loaded and should stay @@ -150,7 +138,9 @@ def update(self, data_folder=None, rpc=None, chain=None): else: logger.warn( "Couldn't find wallet %s in core's wallets.\ -Silently ignored!" % wallet_alias) +Silently ignored!" + % wallet_alias + ) # only ignore rpc errors except RpcError as e: logger.error(f"Failed updating wallet manager. RPC error: {e}") @@ -184,21 +174,24 @@ def create_wallet(self, name, sigs_required, key_type, keys, devices): walletsindir = [] wallet_alias = alias(name) i = 2 - while os.path.isfile( - os.path.join(self.working_folder, "%s.json" % wallet_alias) - ) or os.path.join(self.rpc_path, wallet_alias) in walletsindir: + while ( + os.path.isfile(os.path.join(self.working_folder, "%s.json" % wallet_alias)) + or os.path.join(self.rpc_path, wallet_alias) in walletsindir + ): wallet_alias = alias("%s %d" % (name, i)) i += 1 arr = key_type.split("-") - descs = [key.metadata['combined'] for key in keys] + descs = [key.metadata["combined"] for key in keys] recv_descs = ["%s/0/*" % desc for desc in descs] change_descs = ["%s/1/*" % desc for desc in descs] if len(keys) > 1: - recv_descriptor = "sortedmulti({},{})" \ - .format(sigs_required, ",".join(recv_descs)) - change_descriptor = "sortedmulti({},{})" \ - .format(sigs_required, ",".join(change_descs)) + recv_descriptor = "sortedmulti({},{})".format( + sigs_required, ",".join(recv_descs) + ) + change_descriptor = "sortedmulti({},{})".format( + sigs_required, ",".join(change_descs) + ) else: recv_descriptor = recv_descs[0] change_descriptor = change_descs[0] @@ -213,14 +206,13 @@ def create_wallet(self, name, sigs_required, key_type, keys, devices): w = Wallet( name, wallet_alias, - "{} of {} {}".format( - sigs_required, - len(keys), purposes[key_type] - ) if len(keys) > 1 else purposes[key_type], + "{} of {} {}".format(sigs_required, len(keys), purposes[key_type]) + if len(keys) > 1 + else purposes[key_type], addrtypes[key_type], - '', + "", -1, - '', + "", -1, 0, 0, @@ -232,7 +224,7 @@ def create_wallet(self, name, sigs_required, key_type, keys, devices): {}, os.path.join(self.working_folder, "%s.json" % wallet_alias), self.device_manager, - self + self, ) # save wallet file to disk if self.working_folder is not None: @@ -241,13 +233,15 @@ def create_wallet(self, name, sigs_required, key_type, keys, devices): self.wallets[name] = w return w - def delete_wallet(self, wallet, bitcoin_datadir=get_default_datadir(), chain='main'): + def delete_wallet( + self, wallet, bitcoin_datadir=get_default_datadir(), chain="main" + ): logger.info("Deleting {}".format(wallet.alias)) wallet_rpc_path = os.path.join(self.rpc_path, wallet.alias) self.rpc.unloadwallet(wallet_rpc_path) # Try deleting wallet folder if bitcoin_datadir: - if chain != 'main': + if chain != "main": bitcoin_datadir = os.path.join(bitcoin_datadir, chain) candidates = [ os.path.join(bitcoin_datadir, wallet_rpc_path), @@ -274,15 +268,14 @@ def rename_wallet(self, wallet, name): def full_txlist(self, idx, validate_merkle_proofs=False): txlists = [ [ - { - **tx, - 'wallet_alias': wallet.alias - } for tx in wallet.txlist( + {**tx, "wallet_alias": wallet.alias} + for tx in wallet.txlist( idx, wallet_tx_batch=100 // len(self.wallets), validate_merkle_proofs=validate_merkle_proofs, ) - ] for wallet in self.wallets.values() + ] + for wallet in self.wallets.values() ] result = [] for txlist in txlists: diff --git a/tests/conftest.py b/tests/conftest.py index 20c7a86a7d..09c5f011c9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,61 +9,83 @@ import pytest import docker -from cryptoadvance.specter.bitcoind import BitcoindDockerController, BitcoindPlainController +from cryptoadvance.specter.bitcoind import ( + BitcoindDockerController, + BitcoindPlainController, +) from cryptoadvance.specter.device_manager import DeviceManager from cryptoadvance.specter.specter import Specter from cryptoadvance.specter.server import create_app, init_app def pytest_addoption(parser): - ''' Internally called to add options to pytest - see pytest_generate_tests(metafunc) on how to check that - ''' + """Internally called to add options to pytest + see pytest_generate_tests(metafunc) on how to check that + """ parser.addoption("--docker", action="store_true", help="run bitcoind in docker") - parser.addoption('--bitcoind-version', action='store', default='v0.19.1', help='setup environment: development') + parser.addoption( + "--bitcoind-version", + action="store", + default="v0.19.1", + help="setup environment: development", + ) + def pytest_generate_tests(metafunc): - #ToDo: use custom compiled version of bitcoind + # 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="module") + metafunc.parametrize("docker", [True], scope="module") else: metafunc.parametrize("docker", [False], scope="module") + @pytest.fixture(scope="module") def bitcoin_regtest(docker, request): - #logging.getLogger().setLevel(logging.DEBUG) + # logging.getLogger().setLevel(logging.DEBUG) requested_version = request.config.getoption("--bitcoind-version") if docker: - bitcoind_controller = BitcoindDockerController(rpcport=18543, docker_tag=requested_version) + bitcoind_controller = BitcoindDockerController( + rpcport=18543, docker_tag=requested_version + ) else: - if os.path.isfile('tests/bitcoin/src/bitcoind'): - bitcoind_controller = BitcoindPlainController(bitcoind_path='tests/bitcoin/src/bitcoind') # always prefer the self-compiled bitcoind if existing + if os.path.isfile("tests/bitcoin/src/bitcoind"): + bitcoind_controller = BitcoindPlainController( + bitcoind_path="tests/bitcoin/src/bitcoind" + ) # always prefer the self-compiled bitcoind if existing else: - bitcoind_controller = BitcoindPlainController() # Alternatively take the one on the path for now + bitcoind_controller = ( + BitcoindPlainController() + ) # Alternatively take the one on the path for now bitcoind_controller.start_bitcoind(cleanup_at_exit=True) 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)) + 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 @pytest.fixture def empty_data_folder(): # Make sure that this folder never ever gets a reasonable non-testing use-case - data_folder = './test_specter_data_2789334' + data_folder = "./test_specter_data_2789334" shutil.rmtree(data_folder, ignore_errors=True) os.mkdir(data_folder) yield data_folder shutil.rmtree(data_folder, ignore_errors=True) + @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(''' + 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", @@ -126,9 +148,11 @@ def devices_filled_data_folder(empty_data_folder): } ] } -''') - with open(empty_data_folder+"/devices/specter.json", "w") as text_file: - text_file.write(''' +""" + ) + with open(empty_data_folder + "/devices/specter.json", "w") as text_file: + text_file.write( + """ { "name": "Specter", "type": "specter", @@ -170,15 +194,20 @@ def devices_filled_data_folder(empty_data_folder): } ] } -''') - return empty_data_folder # no longer empty, though - +""" + ) + return empty_data_folder # no longer empty, though + @pytest.fixture def wallets_filled_data_folder(devices_filled_data_folder): - 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(''' + 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( + """ { "alias": "simple", "fullpath": "/home/kim/.specter/wallets/regtest/simple.json", @@ -205,17 +234,20 @@ def wallets_filled_data_folder(devices_filled_data_folder): "address_type": "bech32" } -''') - return devices_filled_data_folder # and with wallets obviously +""" + ) + 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")) + return DeviceManager(os.path.join(devices_filled_data_folder, "devices")) + @pytest.fixture def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder): # Make sure that this folder never ever gets a reasonable non-testing use-case - data_folder = './test_specter_data_3456778' + data_folder = "./test_specter_data_3456778" shutil.rmtree(data_folder, ignore_errors=True) config = { "rpc": { @@ -224,9 +256,9 @@ def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder): "password": bitcoin_regtest.rpcconn.rpcpassword, "port": bitcoin_regtest.rpcconn.rpcport, "host": bitcoin_regtest.rpcconn.ipaddress, - "protocol": "http" + "protocol": "http", }, - "auth": "rpcpasswordaspin" + "auth": "rpcpasswordaspin", } specter = Specter(data_folder=devices_filled_data_folder, config=config) specter.check() @@ -236,17 +268,18 @@ def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder): @pytest.fixture def app(specter_regtest_configured): - ''' the Flask-App, but uninitialized ''' + """the Flask-App, but uninitialized""" app = create_app() app.app_context().push() - app.config["TESTING"]=True + app.config["TESTING"] = True app.testing = True app.tor_service_id = None app.tor_enabled = False init_app(app, specter=specter_regtest_configured) return app + @pytest.fixture def client(app): - ''' a test_client from an initialized Flask-App ''' + """a test_client from an initialized Flask-App""" return app.test_client() diff --git a/tests/test_bitcoind.py b/tests/test_bitcoind.py index 5a3df96133..d3b47aa9e1 100644 --- a/tests/test_bitcoind.py +++ b/tests/test_bitcoind.py @@ -4,12 +4,15 @@ from cryptoadvance.specter.bitcoind import BitcoindPlainController from cryptoadvance.specter.bitcoind import BitcoindDockerController + def test_bitcoinddocker_running(caplog, docker, request): caplog.set_level(logging.INFO) - caplog.set_level(logging.DEBUG,logger="cryptoadvance.specter") + caplog.set_level(logging.DEBUG, logger="cryptoadvance.specter") requested_version = request.config.getoption("--bitcoind-version") if docker: - my_bitcoind = BitcoindDockerController(rpcport=18999, docker_tag=requested_version) # completly different port to not interfere + my_bitcoind = BitcoindDockerController( + rpcport=18999, docker_tag=requested_version + ) # completly different port to not interfere else: try: which("bitcoind") @@ -18,13 +21,17 @@ def test_bitcoinddocker_running(caplog, docker, request): # Doesn't make sense to print anything as this won't be shown # for passing tests return - if os.path.isfile('tests/bitcoin/src/bitcoind'): + if os.path.isfile("tests/bitcoin/src/bitcoind"): # copied from conftest.py # always prefer the self-compiled bitcoind if existing - my_bitcoind = BitcoindPlainController(bitcoind_path='tests/bitcoin/src/bitcoind') + my_bitcoind = BitcoindPlainController( + bitcoind_path="tests/bitcoin/src/bitcoind" + ) else: - my_bitcoind = BitcoindPlainController() # Alternatively take the one on the path for now - + my_bitcoind = ( + BitcoindPlainController() + ) # Alternatively take the one on the path for now + rpcconn = my_bitcoind.start_bitcoind(cleanup_at_exit=True) requested_version = request.config.getoption("--bitcoind-version") assert my_bitcoind.version() == requested_version @@ -35,6 +42,3 @@ def test_bitcoinddocker_running(caplog, docker, request): random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r" my_bitcoind.testcoin_faucet(random_address, amount=25, mine_tx=True) my_bitcoind.stop_bitcoind() - - - \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index 8157252f2a..a8879c985f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,9 +1,10 @@ import logging from cryptoadvance.specter.bitcoind import fetch_wallet_addresses_for_mining + def test_fetch_wallet_addresses_for_mining(caplog, wallets_filled_data_folder): caplog.set_level(logging.INFO) - caplog.set_level(logging.DEBUG,logger="cryptoadvance.specter") + caplog.set_level(logging.DEBUG, logger="cryptoadvance.specter") # Todo: instantiate a specter-testwallet addresses = fetch_wallet_addresses_for_mining(wallets_filled_data_folder) - assert addresses # make more sense out of this test + assert addresses # make more sense out of this test diff --git a/tests/test_controller.py b/tests/test_controller.py index 2332823c4c..5e0bb8d161 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -1,47 +1,50 @@ import logging import pytest + def test_home(caplog, client): - ''' The root of the app ''' + """The root of the app""" caplog.set_level(logging.INFO) - caplog.set_level(logging.DEBUG,logger="cryptoadvance.specter") - login(client, 'secret') - result = client.get('/') + caplog.set_level(logging.DEBUG, logger="cryptoadvance.specter") + login(client, "secret") + result = client.get("/") # By default there is no authentication - assert result.status_code == 200 # OK. - assert b'Welcome to Specter' in result.data - result = client.get('/new_device', follow_redirects=True) - assert result.status_code == 200 # OK. - assert b'Setting up a new device' in result.data - result = client.get('/settings', follow_redirects=True) - assert result.status_code == 200 # OK. - assert b'settings - Specter Desktop' in result.data - result = client.get('/new_wallet', follow_redirects=True) - assert result.status_code == 200 # OK. - assert b'Select the type of the wallet' in result.data + assert result.status_code == 200 # OK. + assert b"Welcome to Specter" in result.data + result = client.get("/new_device", follow_redirects=True) + assert result.status_code == 200 # OK. + assert b"Setting up a new device" in result.data + result = client.get("/settings", follow_redirects=True) + assert result.status_code == 200 # OK. + assert b"settings - Specter Desktop" in result.data + result = client.get("/new_wallet", follow_redirects=True) + assert result.status_code == 200 # OK. + assert b"Select the type of the wallet" in result.data # Login logout testing - result = client.get('/login', follow_redirects=False) + result = client.get("/login", follow_redirects=False) assert result.status_code == 200 - assert b'Password' in result.data - result = login(client, 'secret') - assert b'Logged in successfully.' in result.data + assert b"Password" in result.data + result = login(client, "secret") + assert b"Logged in successfully." in result.data result = logout(client) - assert b'You were logged out' in result.data - result = login(client, 'non_valid_password') - assert b'Invalid username or password' in result.data - result = login(client, 'blub') - assert b'Invalid username or password' in result.data + assert b"You were logged out" in result.data + result = login(client, "non_valid_password") + assert b"Invalid username or password" in result.data + result = login(client, "blub") + assert b"Invalid username or password" in result.data def login(client, password): - ''' login helper-function ''' - result = client.post('/login', data=dict( - password=password - ), follow_redirects=True) - assert b'We could not check your password, maybe Bitcoin Core is not running or not configured?' not in result.data + """login helper-function""" + result = client.post("/login", data=dict(password=password), follow_redirects=True) + assert ( + b"We could not check your password, maybe Bitcoin Core is not running or not configured?" + not in result.data + ) return result + def logout(client): - ''' logout helper-method ''' - return client.get('/logout', follow_redirects=True) + """logout helper-method""" + return client.get("/logout", follow_redirects=True) diff --git a/tests/test_device_manager.py b/tests/test_device_manager.py index f97c927d89..e3d4a0047f 100644 --- a/tests/test_device_manager.py +++ b/tests/test_device_manager.py @@ -6,64 +6,66 @@ def test_DeviceManager(empty_data_folder): - # A DeviceManager manages devices, specifically the persistence + # A DeviceManager manages devices, specifically the persistence # of them via json-files in an empty data folder dm = DeviceManager(data_folder=empty_data_folder) # initialization will load from the folder but it's empty at first assert len(dm.devices) == 0 # a device has a name, a type and a list of keys a_key = Key( - 'Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM', - '08686ac6', - 'm/48h/1h/0h/2h', - 'wsh', - 'tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL' + "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", + "08686ac6", + "m/48h/1h/0h/2h", + "wsh", + "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL", ) # the DeviceManager doesn't care so much about the content of a key # so this is a minimal valid "key": - another_key = Key.from_json({ - 'original': 'tpubDDZ5jjGT5RvrAyjoLZfdCfv1PAPmicnhNctwZGKiCMF1Zy5hCGMqppxwYZzWgvPqk7LucMMHo7rkB6Dyj5ZLd2W62FAEP3U6pV4jD5gb9ma' - }) - dm.add_device("some_name","the_type",[a_key, another_key]) + another_key = Key.from_json( + { + "original": "tpubDDZ5jjGT5RvrAyjoLZfdCfv1PAPmicnhNctwZGKiCMF1Zy5hCGMqppxwYZzWgvPqk7LucMMHo7rkB6Dyj5ZLd2W62FAEP3U6pV4jD5gb9ma" + } + ) + dm.add_device("some_name", "the_type", [a_key, another_key]) # A json file was generated for the new device: - assert os.path.isfile(dm.devices['some_name'].fullpath) + assert os.path.isfile(dm.devices["some_name"].fullpath) # You can access the new device either by its name of with `get_by_alias` by its alias - assert dm.get_by_alias('some_name').name == 'some_name' + assert dm.get_by_alias("some_name").name == "some_name" # unknown device is replaced by 'other' - assert dm.get_by_alias('some_name').device_type == 'other' - assert dm.get_by_alias('some_name').keys[0].fingerprint == '08686ac6' + assert dm.get_by_alias("some_name").device_type == "other" + assert dm.get_by_alias("some_name").keys[0].fingerprint == "08686ac6" # Now it has a length of 1 assert len(dm.devices) == 1 # and is iterable - assert [the_type.device_type for the_type in dm.devices.values()] == ['other'] + assert [the_type.device_type for the_type in dm.devices.values()] == ["other"] # The DeviceManager will return Device-Types (subclass of dict) # any unknown type is replaced by GenericDevice - assert type(dm.devices['some_name']) == GenericDevice + assert type(dm.devices["some_name"]) == GenericDevice # The DeviceManager also has a `devices_names` property, returning a sorted list of the names of all devices - assert dm.devices_names == ['some_name'] - dm.add_device("another_name","the_type",[a_key, another_key]) - assert dm.devices_names == ['another_name', 'some_name'] + assert dm.devices_names == ["some_name"] + dm.add_device("another_name", "the_type", [a_key, another_key]) + assert dm.devices_names == ["another_name", "some_name"] # You can also remove a device - which will delete its json and remove it from the manager - another_device_fullpath = dm.devices['another_name'].fullpath + another_device_fullpath = dm.devices["another_name"].fullpath assert os.path.isfile(another_device_fullpath) - dm.remove_device(dm.devices['another_name']) + dm.remove_device(dm.devices["another_name"]) assert not os.path.isfile(another_device_fullpath) assert len(dm.devices) == 1 - assert dm.devices_names == ['some_name'] + assert dm.devices_names == ["some_name"] - # A device is mainly a Domain-Object which assumes an underlying + # A device is mainly a Domain-Object which assumes an underlying # json-file which can be found in the "fullpath"-key # It derives from a dict # It needs a DeviceManager to be injected and can't reasonable # be created on your own. # It has 5 dict keys: `fullpath`, `alias`, `name`, `type`, `keys` - some_device = dm.devices['some_name'] - assert some_device.fullpath == empty_data_folder + '/some_name.json' - assert some_device.alias == 'some_name' - assert some_device.name == 'some_name' - assert some_device.device_type == 'other' + some_device = dm.devices["some_name"] + assert some_device.fullpath == empty_data_folder + "/some_name.json" + assert some_device.alias == "some_name" + assert some_device.name == "some_name" + assert some_device.device_type == "other" assert len(some_device.keys) == 2 assert some_device.keys[0] == a_key assert some_device.keys[1] == another_key @@ -71,9 +73,11 @@ def test_DeviceManager(empty_data_folder): # Keys can be added and removed. It will instantly update the underlying json # Adding keys can be done by passing an array of keys object to the `add_keys` method of a device # A key dict must contain an `original` property - third_key = Key.from_json({ - 'original': 'tpubDEmTg3b5aPNFnkHXx481F3h9dPSVJiyvqV24dBMXWncoRRu6VJzPDeEtQ4H7EnRtLbn2aPkxhTn8odWXsXkSRDdmAvCCrPmfjfPSVswfDhg' - }) + third_key = Key.from_json( + { + "original": "tpubDEmTg3b5aPNFnkHXx481F3h9dPSVJiyvqV24dBMXWncoRRu6VJzPDeEtQ4H7EnRtLbn2aPkxhTn8odWXsXkSRDdmAvCCrPmfjfPSVswfDhg" + } + ) some_device.add_keys([third_key]) assert len(some_device.keys) == 3 assert some_device.keys[0] == a_key @@ -99,15 +103,24 @@ def test_DeviceManager(empty_data_folder): assert some_device.keys[0] == a_key assert some_device.keys[1] == another_key + def test_device_wallets(bitcoin_regtest, devices_filled_data_folder, device_manager): - wm = WalletManager(devices_filled_data_folder,bitcoin_regtest.get_rpc(), "regtest", device_manager) - device = device_manager.get_by_alias('trezor') + wm = WalletManager( + devices_filled_data_folder, bitcoin_regtest.get_rpc(), "regtest", device_manager + ) + device = device_manager.get_by_alias("trezor") assert len(device.wallets(wm)) == 0 - wallet = wm.create_wallet('a_test_wallet', 1, 'wpkh', [device.keys[5]], [device]) + wallet = wm.create_wallet("a_test_wallet", 1, "wpkh", [device.keys[5]], [device]) assert len(device.wallets(wm)) == 1 assert device.wallets(wm)[0].alias == wallet.alias - second_device = device_manager.get_by_alias('specter') - multisig_wallet = wm.create_wallet('a_multisig_test_wallet', 1, 'wsh', [device.keys[7], second_device.keys[0]], [device, second_device]) + second_device = device_manager.get_by_alias("specter") + multisig_wallet = wm.create_wallet( + "a_multisig_test_wallet", + 1, + "wsh", + [device.keys[7], second_device.keys[0]], + [device, second_device], + ) assert len(device.wallets(wm)) == 2 assert device.wallets(wm)[0].alias == wallet.alias diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 99237a3119..3e59ff1293 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,34 +1,39 @@ import logging + def test_load_jsons(caplog): caplog.set_level(logging.INFO) - caplog.set_level(logging.DEBUG,logger="cryptoadvance.specter") + caplog.set_level(logging.DEBUG, logger="cryptoadvance.specter") import cryptoadvance.specter.helpers as helpers + mydict = helpers.load_jsons("./tests/helpers_testdata") assert mydict["some_jsonfile"]["blub"] == "bla" assert mydict["some_other_jsonfile"]["bla"] == "blub" - mydict = helpers.load_jsons("./tests/helpers_testdata",'id') + mydict = helpers.load_jsons("./tests/helpers_testdata", "id") assert "some_jsonfile" not in mydict # instead the value for the key "id" is now used as the top-level key - assert mydict["ID123"]['blub'] == "bla" - # This also assumes that the key is unique!!!! - assert mydict["ID124"]['bla'] == "blub" + assert mydict["ID123"]["blub"] == "bla" + # This also assumes that the key is unique!!!! + assert mydict["ID124"]["bla"] == "blub" # ToDo: check the uniqueness in the implementation to avoid issues # the filename is added as alias - assert mydict["ID123"]['alias'] == "some_jsonfile" + assert mydict["ID123"]["alias"] == "some_jsonfile" # We also get the fullpath of that file: - assert mydict["ID123"]['fullpath'] == "./tests/helpers_testdata/some_jsonfile.json" + assert mydict["ID123"]["fullpath"] == "./tests/helpers_testdata/some_jsonfile.json" # Quite handy if you want to get rid of it which is as easy as: # os.remove(mydict["ID123"]['fullpath']) + def test_which(caplog): caplog.set_level(logging.INFO) - caplog.set_level(logging.DEBUG,logger="cryptoadvance.specter") + caplog.set_level(logging.DEBUG, logger="cryptoadvance.specter") import cryptoadvance.specter.helpers as helpers + try: helpers.which("some_non_existing_binary") assert False, "Should raise an Exception" except: pass - assert helpers.which("date") == "/bin/date" or helpers.which("date") == "/usr/bin/date" # travis-CI has it on /bin/date - \ No newline at end of file + assert ( + helpers.which("date") == "/bin/date" or helpers.which("date") == "/usr/bin/date" + ) # travis-CI has it on /bin/date diff --git a/tests/test_hwibridge.py b/tests/test_hwibridge.py index 34949f8b4e..503a299e44 100644 --- a/tests/test_hwibridge.py +++ b/tests/test_hwibridge.py @@ -1,121 +1,174 @@ import json, requests + def test_malformed_parse_error(client): - client.environ_base['HTTP_ORIGIN'] = 'http://127.0.0.1:25441/' - req = client.post('http://127.0.0.1:25441/hwi/api/', data=b'malformed') - assert { + client.environ_base["HTTP_ORIGIN"] = "http://127.0.0.1:25441/" + req = client.post("http://127.0.0.1:25441/hwi/api/", data=b"malformed") + assert { "jsonrpc": "2.0", - "error": { "code": -32700, "message": "Parse error" }, - "id": None + "error": {"code": -32700, "message": "Parse error"}, + "id": None, } == json.loads(req.data) + def test_call_unauthorized_origin(client): - client.environ_base['HTTP_ORIGIN'] = 'http://unauthorized_domain.com/' - req = client.post('http://127.0.0.1:25441/hwi/api/', json={ - 'jsonrpc': '2.0', - 'method': 'enumerate', - 'id': 1, - 'params': {}, - 'forwarded_request': True - }) - assert { + client.environ_base["HTTP_ORIGIN"] = "http://unauthorized_domain.com/" + req = client.post( + "http://127.0.0.1:25441/hwi/api/", + json={ + "jsonrpc": "2.0", + "method": "enumerate", + "id": 1, + "params": {}, + "forwarded_request": True, + }, + ) + assert { "jsonrpc": "2.0", - "error": { "code": -32001, "message": "Unauthorized request origin.
You must first whitelist this website URL in HWIBridge settings to grant it access." }, - "id": None + "error": { + "code": -32001, + "message": "Unauthorized request origin.
You must first whitelist this website URL in HWIBridge settings to grant it access.", + }, + "id": None, } == json.loads(req.data) + def test_call_without_method(client): - client.environ_base['HTTP_ORIGIN'] = 'http://127.0.0.1:25441/' - req = client.post('http://127.0.0.1:25441/hwi/api/', json={ - 'jsonrpc': '2.0', - 'id': 1, - 'params': {}, - 'forwarded_request': True - }) - assert { + client.environ_base["HTTP_ORIGIN"] = "http://127.0.0.1:25441/" + req = client.post( + "http://127.0.0.1:25441/hwi/api/", + json={"jsonrpc": "2.0", "id": 1, "params": {}, "forwarded_request": True}, + ) + assert { "jsonrpc": "2.0", - "error": { "code": -32600, "message": "Invalid Request. Request must specify a 'method'." }, - "id": 1 + "error": { + "code": -32600, + "message": "Invalid Request. Request must specify a 'method'.", + }, + "id": 1, } == json.loads(req.data) + def test_call_non_existing_method(client): - client.environ_base['HTTP_ORIGIN'] = 'http://127.0.0.1:25441/' - req = client.post('http://127.0.0.1:25441/hwi/api/', json={ - 'jsonrpc': '2.0', - 'method': 'enumerate_2', - 'id': 1, - 'params': {}, - 'forwarded_request': True - }) - assert { + client.environ_base["HTTP_ORIGIN"] = "http://127.0.0.1:25441/" + req = client.post( + "http://127.0.0.1:25441/hwi/api/", + json={ + "jsonrpc": "2.0", + "method": "enumerate_2", + "id": 1, + "params": {}, + "forwarded_request": True, + }, + ) + assert { "jsonrpc": "2.0", - "error": { "code": -32601, "message": "Method not found." }, - "id": 1 + "error": {"code": -32601, "message": "Method not found."}, + "id": 1, } == json.loads(req.data) + def test_enumerate_request_success(client): - client.environ_base['HTTP_ORIGIN'] = 'http://127.0.0.1:25441/' - req = client.post('http://127.0.0.1:25441/hwi/api/', json={ - 'jsonrpc': '2.0', - 'method': 'enumerate', - 'id': 1, - 'params': {}, - 'forwarded_request': True - }) - assert { "id": 1, "jsonrpc": "2.0", "result": [] } == json.loads(req.data) + client.environ_base["HTTP_ORIGIN"] = "http://127.0.0.1:25441/" + req = client.post( + "http://127.0.0.1:25441/hwi/api/", + json={ + "jsonrpc": "2.0", + "method": "enumerate", + "id": 1, + "params": {}, + "forwarded_request": True, + }, + ) + assert {"id": 1, "jsonrpc": "2.0", "result": []} == json.loads(req.data) + def test_request_success_localhost_origin(client): - client.environ_base['HTTP_ORIGIN'] = 'http://localhost:25441/' - req = client.post('http://127.0.0.1:25441/hwi/api/', json={ - 'jsonrpc': '2.0', - 'method': 'enumerate', - 'id': 1, - 'params': {}, - 'forwarded_request': True - }) - assert { "id": 1, "jsonrpc": "2.0", "result": [] } == json.loads(req.data) + client.environ_base["HTTP_ORIGIN"] = "http://localhost:25441/" + req = client.post( + "http://127.0.0.1:25441/hwi/api/", + json={ + "jsonrpc": "2.0", + "method": "enumerate", + "id": 1, + "params": {}, + "forwarded_request": True, + }, + ) + assert {"id": 1, "jsonrpc": "2.0", "result": []} == json.loads(req.data) + def test_calling_method_with_non_existing_parameters(client): - client.environ_base['HTTP_ORIGIN'] = 'http://127.0.0.1:25441/' - req = client.post('http://127.0.0.1:25441/hwi/api/', json={ - 'jsonrpc': '2.0', - 'method': 'enumerate', - 'id': 1, - 'params': { 'non_existing_parameter': True }, - 'forwarded_request': True - }) - assert { + client.environ_base["HTTP_ORIGIN"] = "http://127.0.0.1:25441/" + req = client.post( + "http://127.0.0.1:25441/hwi/api/", + json={ + "jsonrpc": "2.0", + "method": "enumerate", + "id": 1, + "params": {"non_existing_parameter": True}, + "forwarded_request": True, + }, + ) + assert { "jsonrpc": "2.0", - "error": { "code": -32000, "message": "Internal error: enumerate() got an unexpected keyword argument 'non_existing_parameter'." }, - "id": 1 + "error": { + "code": -32000, + "message": "Internal error: enumerate() got an unexpected keyword argument 'non_existing_parameter'.", + }, + "id": 1, } == json.loads(req.data) + def test_call_not_connected_device(client): - client.environ_base['HTTP_ORIGIN'] = 'http://127.0.0.1:25441/' - req = client.post('http://127.0.0.1:25441/hwi/api/', json={ - 'jsonrpc': '2.0', - 'method': 'prompt_pin', - 'id': 1, - 'params': { 'device_type': 'trezor', 'path': '', 'passphrase': '', 'chain': 'test' }, - 'forwarded_request': True - }) - assert { + client.environ_base["HTTP_ORIGIN"] = "http://127.0.0.1:25441/" + req = client.post( + "http://127.0.0.1:25441/hwi/api/", + json={ + "jsonrpc": "2.0", + "method": "prompt_pin", + "id": 1, + "params": { + "device_type": "trezor", + "path": "", + "passphrase": "", + "chain": "test", + }, + "forwarded_request": True, + }, + ) + assert { "jsonrpc": "2.0", - "error": { "code": -32000, "message": "Internal error: The device could not be found. Please check it is properly connected and try again." }, - "id": 1 + "error": { + "code": -32000, + "message": "Internal error: The device could not be found. Please check it is properly connected and try again.", + }, + "id": 1, } == json.loads(req.data) + def test_call_prompt_pin_invalid_device(client): - client.environ_base['HTTP_ORIGIN'] = 'http://127.0.0.1:25441/' - req = client.post('http://127.0.0.1:25441/hwi/api/', json={ - 'jsonrpc': '2.0', - 'method': 'prompt_pin', - 'id': 1, - 'params': { 'device_type': 'ledger', 'path': '', 'passphrase': '', 'chain': 'test' }, - 'forwarded_request': True - }) - assert { + client.environ_base["HTTP_ORIGIN"] = "http://127.0.0.1:25441/" + req = client.post( + "http://127.0.0.1:25441/hwi/api/", + json={ + "jsonrpc": "2.0", + "method": "prompt_pin", + "id": 1, + "params": { + "device_type": "ledger", + "path": "", + "passphrase": "", + "chain": "test", + }, + "forwarded_request": True, + }, + ) + assert { "jsonrpc": "2.0", - "error": { "code": -32000, "message": "Internal error: Invalid HWI device type ledger, prompt_pin is only supported for Trezor and Keepkey devices." }, - "id": 1 + "error": { + "code": -32000, + "message": "Internal error: Invalid HWI device type ledger, prompt_pin is only supported for Trezor and Keepkey devices.", + }, + "id": 1, } == json.loads(req.data) diff --git a/tests/test_merkleblock.py b/tests/test_merkleblock.py index bff9f9bbf5..a7f348d0aa 100644 --- a/tests/test_merkleblock.py +++ b/tests/test_merkleblock.py @@ -2,153 +2,198 @@ from unittest import TestCase from io import BytesIO -from cryptoadvance.specter.util.merkleblock import MerkleTree, Block, MerkleBlock, little_endian_to_int +from cryptoadvance.specter.util.merkleblock import ( + MerkleTree, + Block, + MerkleBlock, + little_endian_to_int, +) class BlockTest(TestCase): - def test_parse(self): - block_raw = bytes.fromhex('020000208ec39428b17323fa0ddec8e887b4a7c53b8c0a0a220cfd0000000000000000005b0750fce0a889502d40508d39576821155e9c9e3f5c3157f961db38fd8b25be1e77a759e93c0118a4ffd71d') + block_raw = bytes.fromhex( + "020000208ec39428b17323fa0ddec8e887b4a7c53b8c0a0a220cfd0000000000000000005b0750fce0a889502d40508d39576821155e9c9e3f5c3157f961db38fd8b25be1e77a759e93c0118a4ffd71d" + ) stream = BytesIO(block_raw) block = Block.parse_header(stream) self.assertEqual(block.version, 0x20000002) - want = bytes.fromhex('000000000000000000fd0c220a0a8c3bc5a7b487e8c8de0dfa2373b12894c38e') + want = bytes.fromhex( + "000000000000000000fd0c220a0a8c3bc5a7b487e8c8de0dfa2373b12894c38e" + ) self.assertEqual(block.prev_block, want) - want = bytes.fromhex('be258bfd38db61f957315c3f9e9c5e15216857398d50402d5089a8e0fc50075b') + want = bytes.fromhex( + "be258bfd38db61f957315c3f9e9c5e15216857398d50402d5089a8e0fc50075b" + ) self.assertEqual(block.merkle_root, want) - self.assertEqual(block.timestamp, 0x59a7771e) - self.assertEqual(block.bits, bytes.fromhex('e93c0118')) - self.assertEqual(block.nonce, bytes.fromhex('a4ffd71d')) + self.assertEqual(block.timestamp, 0x59A7771E) + self.assertEqual(block.bits, bytes.fromhex("e93c0118")) + self.assertEqual(block.nonce, bytes.fromhex("a4ffd71d")) def test_serialize(self): - block_raw = bytes.fromhex('020000208ec39428b17323fa0ddec8e887b4a7c53b8c0a0a220cfd0000000000000000005b0750fce0a889502d40508d39576821155e9c9e3f5c3157f961db38fd8b25be1e77a759e93c0118a4ffd71d') + block_raw = bytes.fromhex( + "020000208ec39428b17323fa0ddec8e887b4a7c53b8c0a0a220cfd0000000000000000005b0750fce0a889502d40508d39576821155e9c9e3f5c3157f961db38fd8b25be1e77a759e93c0118a4ffd71d" + ) stream = BytesIO(block_raw) block = Block.parse_header(stream) self.assertEqual(block.serialize(), block_raw) def test_hash(self): - block_raw = bytes.fromhex('020000208ec39428b17323fa0ddec8e887b4a7c53b8c0a0a220cfd0000000000000000005b0750fce0a889502d40508d39576821155e9c9e3f5c3157f961db38fd8b25be1e77a759e93c0118a4ffd71d') + block_raw = bytes.fromhex( + "020000208ec39428b17323fa0ddec8e887b4a7c53b8c0a0a220cfd0000000000000000005b0750fce0a889502d40508d39576821155e9c9e3f5c3157f961db38fd8b25be1e77a759e93c0118a4ffd71d" + ) stream = BytesIO(block_raw) block = Block.parse_header(stream) - self.assertEqual(block.hash(), bytes.fromhex('0000000000000000007e9e4c586439b0cdbe13b1370bdd9435d76a644d047523')) + self.assertEqual( + block.hash(), + bytes.fromhex( + "0000000000000000007e9e4c586439b0cdbe13b1370bdd9435d76a644d047523" + ), + ) def test_bip9(self): - block_raw = bytes.fromhex('020000208ec39428b17323fa0ddec8e887b4a7c53b8c0a0a220cfd0000000000000000005b0750fce0a889502d40508d39576821155e9c9e3f5c3157f961db38fd8b25be1e77a759e93c0118a4ffd71d') + block_raw = bytes.fromhex( + "020000208ec39428b17323fa0ddec8e887b4a7c53b8c0a0a220cfd0000000000000000005b0750fce0a889502d40508d39576821155e9c9e3f5c3157f961db38fd8b25be1e77a759e93c0118a4ffd71d" + ) stream = BytesIO(block_raw) block = Block.parse_header(stream) self.assertTrue(block.bip9()) - block_raw = bytes.fromhex('0400000039fa821848781f027a2e6dfabbf6bda920d9ae61b63400030000000000000000ecae536a304042e3154be0e3e9a8220e5568c3433a9ab49ac4cbb74f8df8e8b0cc2acf569fb9061806652c27') + block_raw = bytes.fromhex( + "0400000039fa821848781f027a2e6dfabbf6bda920d9ae61b63400030000000000000000ecae536a304042e3154be0e3e9a8220e5568c3433a9ab49ac4cbb74f8df8e8b0cc2acf569fb9061806652c27" + ) stream = BytesIO(block_raw) block = Block.parse_header(stream) self.assertFalse(block.bip9()) def test_bip91(self): - block_raw = bytes.fromhex('1200002028856ec5bca29cf76980d368b0a163a0bb81fc192951270100000000000000003288f32a2831833c31a25401c52093eb545d28157e200a64b21b3ae8f21c507401877b5935470118144dbfd1') + block_raw = bytes.fromhex( + "1200002028856ec5bca29cf76980d368b0a163a0bb81fc192951270100000000000000003288f32a2831833c31a25401c52093eb545d28157e200a64b21b3ae8f21c507401877b5935470118144dbfd1" + ) stream = BytesIO(block_raw) block = Block.parse_header(stream) self.assertTrue(block.bip91()) - block_raw = bytes.fromhex('020000208ec39428b17323fa0ddec8e887b4a7c53b8c0a0a220cfd0000000000000000005b0750fce0a889502d40508d39576821155e9c9e3f5c3157f961db38fd8b25be1e77a759e93c0118a4ffd71d') + block_raw = bytes.fromhex( + "020000208ec39428b17323fa0ddec8e887b4a7c53b8c0a0a220cfd0000000000000000005b0750fce0a889502d40508d39576821155e9c9e3f5c3157f961db38fd8b25be1e77a759e93c0118a4ffd71d" + ) stream = BytesIO(block_raw) block = Block.parse_header(stream) self.assertFalse(block.bip91()) def test_bip141(self): - block_raw = bytes.fromhex('020000208ec39428b17323fa0ddec8e887b4a7c53b8c0a0a220cfd0000000000000000005b0750fce0a889502d40508d39576821155e9c9e3f5c3157f961db38fd8b25be1e77a759e93c0118a4ffd71d') + block_raw = bytes.fromhex( + "020000208ec39428b17323fa0ddec8e887b4a7c53b8c0a0a220cfd0000000000000000005b0750fce0a889502d40508d39576821155e9c9e3f5c3157f961db38fd8b25be1e77a759e93c0118a4ffd71d" + ) stream = BytesIO(block_raw) block = Block.parse_header(stream) self.assertTrue(block.bip141()) - block_raw = bytes.fromhex('0000002066f09203c1cf5ef1531f24ed21b1915ae9abeb691f0d2e0100000000000000003de0976428ce56125351bae62c5b8b8c79d8297c702ea05d60feabb4ed188b59c36fa759e93c0118b74b2618') + block_raw = bytes.fromhex( + "0000002066f09203c1cf5ef1531f24ed21b1915ae9abeb691f0d2e0100000000000000003de0976428ce56125351bae62c5b8b8c79d8297c702ea05d60feabb4ed188b59c36fa759e93c0118b74b2618" + ) stream = BytesIO(block_raw) block = Block.parse_header(stream) self.assertFalse(block.bip141()) def test_target(self): - block_raw = bytes.fromhex('020000208ec39428b17323fa0ddec8e887b4a7c53b8c0a0a220cfd0000000000000000005b0750fce0a889502d40508d39576821155e9c9e3f5c3157f961db38fd8b25be1e77a759e93c0118a4ffd71d') + block_raw = bytes.fromhex( + "020000208ec39428b17323fa0ddec8e887b4a7c53b8c0a0a220cfd0000000000000000005b0750fce0a889502d40508d39576821155e9c9e3f5c3157f961db38fd8b25be1e77a759e93c0118a4ffd71d" + ) stream = BytesIO(block_raw) block = Block.parse_header(stream) - self.assertEqual(block.target(), 0x13ce9000000000000000000000000000000000000000000) + self.assertEqual( + block.target(), 0x13CE9000000000000000000000000000000000000000000 + ) self.assertEqual(int(block.difficulty()), 888171856257) def test_check_pow(self): - block_raw = bytes.fromhex('04000000fbedbbf0cfdaf278c094f187f2eb987c86a199da22bbb20400000000000000007b7697b29129648fa08b4bcd13c9d5e60abb973a1efac9c8d573c71c807c56c3d6213557faa80518c3737ec1') + block_raw = bytes.fromhex( + "04000000fbedbbf0cfdaf278c094f187f2eb987c86a199da22bbb20400000000000000007b7697b29129648fa08b4bcd13c9d5e60abb973a1efac9c8d573c71c807c56c3d6213557faa80518c3737ec1" + ) stream = BytesIO(block_raw) block = Block.parse_header(stream) self.assertTrue(block.check_pow()) - block_raw = bytes.fromhex('04000000fbedbbf0cfdaf278c094f187f2eb987c86a199da22bbb20400000000000000007b7697b29129648fa08b4bcd13c9d5e60abb973a1efac9c8d573c71c807c56c3d6213557faa80518c3737ec0') + block_raw = bytes.fromhex( + "04000000fbedbbf0cfdaf278c094f187f2eb987c86a199da22bbb20400000000000000007b7697b29129648fa08b4bcd13c9d5e60abb973a1efac9c8d573c71c807c56c3d6213557faa80518c3737ec0" + ) stream = BytesIO(block_raw) block = Block.parse_header(stream) self.assertFalse(block.check_pow()) def test_validate_merkle_root(self): hashes_hex = [ - 'f54cb69e5dc1bd38ee6901e4ec2007a5030e14bdd60afb4d2f3428c88eea17c1', - 'c57c2d678da0a7ee8cfa058f1cf49bfcb00ae21eda966640e312b464414731c1', - 'b027077c94668a84a5d0e72ac0020bae3838cb7f9ee3fa4e81d1eecf6eda91f3', - '8131a1b8ec3a815b4800b43dff6c6963c75193c4190ec946b93245a9928a233d', - 'ae7d63ffcb3ae2bc0681eca0df10dda3ca36dedb9dbf49e33c5fbe33262f0910', - '61a14b1bbdcdda8a22e61036839e8b110913832efd4b086948a6a64fd5b3377d', - 'fc7051c8b536ac87344c5497595d5d2ffdaba471c73fae15fe9228547ea71881', - '77386a46e26f69b3cd435aa4faac932027f58d0b7252e62fb6c9c2489887f6df', - '59cbc055ccd26a2c4c4df2770382c7fea135c56d9e75d3f758ac465f74c025b8', - '7c2bf5687f19785a61be9f46e031ba041c7f93e2b7e9212799d84ba052395195', - '08598eebd94c18b0d59ac921e9ba99e2b8ab7d9fccde7d44f2bd4d5e2e726d2e', - 'f0bb99ef46b029dd6f714e4b12a7d796258c48fee57324ebdc0bbc4700753ab1', + "f54cb69e5dc1bd38ee6901e4ec2007a5030e14bdd60afb4d2f3428c88eea17c1", + "c57c2d678da0a7ee8cfa058f1cf49bfcb00ae21eda966640e312b464414731c1", + "b027077c94668a84a5d0e72ac0020bae3838cb7f9ee3fa4e81d1eecf6eda91f3", + "8131a1b8ec3a815b4800b43dff6c6963c75193c4190ec946b93245a9928a233d", + "ae7d63ffcb3ae2bc0681eca0df10dda3ca36dedb9dbf49e33c5fbe33262f0910", + "61a14b1bbdcdda8a22e61036839e8b110913832efd4b086948a6a64fd5b3377d", + "fc7051c8b536ac87344c5497595d5d2ffdaba471c73fae15fe9228547ea71881", + "77386a46e26f69b3cd435aa4faac932027f58d0b7252e62fb6c9c2489887f6df", + "59cbc055ccd26a2c4c4df2770382c7fea135c56d9e75d3f758ac465f74c025b8", + "7c2bf5687f19785a61be9f46e031ba041c7f93e2b7e9212799d84ba052395195", + "08598eebd94c18b0d59ac921e9ba99e2b8ab7d9fccde7d44f2bd4d5e2e726d2e", + "f0bb99ef46b029dd6f714e4b12a7d796258c48fee57324ebdc0bbc4700753ab1", ] hashes = [bytes.fromhex(x) for x in hashes_hex] - stream = BytesIO(bytes.fromhex('00000020fcb19f7895db08cadc9573e7915e3919fb76d59868a51d995201000000000000acbcab8bcc1af95d8d563b77d24c3d19b18f1486383d75a5085c4e86c86beed691cfa85916ca061a00000000')) + stream = BytesIO( + bytes.fromhex( + "00000020fcb19f7895db08cadc9573e7915e3919fb76d59868a51d995201000000000000acbcab8bcc1af95d8d563b77d24c3d19b18f1486383d75a5085c4e86c86beed691cfa85916ca061a00000000" + ) + ) block = Block.parse_header(stream) block.tx_hashes = hashes self.assertTrue(block.validate_merkle_root()) class MerkleBlockTest(TestCase): - def test_parse(self): - hex_merkle_block = '00000020df3b053dc46f162a9b00c7f0d5124e2676d47bbe7c5d0793a500000000000000ef445fef2ed495c275892206ca533e7411907971013ab83e3b47bd0d692d14d4dc7c835b67d8001ac157e670bf0d00000aba412a0d1480e370173072c9562becffe87aa661c1e4a6dbc305d38ec5dc088a7cf92e6458aca7b32edae818f9c2c98c37e06bf72ae0ce80649a38655ee1e27d34d9421d940b16732f24b94023e9d572a7f9ab8023434a4feb532d2adfc8c2c2158785d1bd04eb99df2e86c54bc13e139862897217400def5d72c280222c4cbaee7261831e1550dbb8fa82853e9fe506fc5fda3f7b919d8fe74b6282f92763cef8e625f977af7c8619c32a369b832bc2d051ecd9c73c51e76370ceabd4f25097c256597fa898d404ed53425de608ac6bfe426f6e2bb457f1c554866eb69dcb8d6bf6f880e9a59b3cd053e6c7060eeacaacf4dac6697dac20e4bd3f38a2ea2543d1ab7953e3430790a9f81e1c67f5b58c825acf46bd02848384eebe9af917274cdfbb1a28a5d58a23a17977def0de10d644258d9c54f886d47d293a411cb6226103b55635' + hex_merkle_block = "00000020df3b053dc46f162a9b00c7f0d5124e2676d47bbe7c5d0793a500000000000000ef445fef2ed495c275892206ca533e7411907971013ab83e3b47bd0d692d14d4dc7c835b67d8001ac157e670bf0d00000aba412a0d1480e370173072c9562becffe87aa661c1e4a6dbc305d38ec5dc088a7cf92e6458aca7b32edae818f9c2c98c37e06bf72ae0ce80649a38655ee1e27d34d9421d940b16732f24b94023e9d572a7f9ab8023434a4feb532d2adfc8c2c2158785d1bd04eb99df2e86c54bc13e139862897217400def5d72c280222c4cbaee7261831e1550dbb8fa82853e9fe506fc5fda3f7b919d8fe74b6282f92763cef8e625f977af7c8619c32a369b832bc2d051ecd9c73c51e76370ceabd4f25097c256597fa898d404ed53425de608ac6bfe426f6e2bb457f1c554866eb69dcb8d6bf6f880e9a59b3cd053e6c7060eeacaacf4dac6697dac20e4bd3f38a2ea2543d1ab7953e3430790a9f81e1c67f5b58c825acf46bd02848384eebe9af917274cdfbb1a28a5d58a23a17977def0de10d644258d9c54f886d47d293a411cb6226103b55635" mb = MerkleBlock.parse(BytesIO(bytes.fromhex(hex_merkle_block))) version = 0x20000000 self.assertEqual(mb.header.version, version) - merkle_root_hex = 'ef445fef2ed495c275892206ca533e7411907971013ab83e3b47bd0d692d14d4' + merkle_root_hex = ( + "ef445fef2ed495c275892206ca533e7411907971013ab83e3b47bd0d692d14d4" + ) merkle_root = bytes.fromhex(merkle_root_hex)[::-1] self.assertEqual(mb.header.merkle_root, merkle_root) - prev_block_hex = 'df3b053dc46f162a9b00c7f0d5124e2676d47bbe7c5d0793a500000000000000' + prev_block_hex = ( + "df3b053dc46f162a9b00c7f0d5124e2676d47bbe7c5d0793a500000000000000" + ) prev_block = bytes.fromhex(prev_block_hex)[::-1] self.assertEqual(mb.header.prev_block, prev_block) - timestamp = little_endian_to_int(bytes.fromhex('dc7c835b')) + timestamp = little_endian_to_int(bytes.fromhex("dc7c835b")) self.assertEqual(mb.header.timestamp, timestamp) - bits = bytes.fromhex('67d8001a') + bits = bytes.fromhex("67d8001a") self.assertEqual(mb.header.bits, bits) - nonce = bytes.fromhex('c157e670') + nonce = bytes.fromhex("c157e670") self.assertEqual(mb.header.nonce, nonce) - total = little_endian_to_int(bytes.fromhex('bf0d0000')) + total = little_endian_to_int(bytes.fromhex("bf0d0000")) self.assertEqual(mb.total, total) hex_hashes = [ - 'ba412a0d1480e370173072c9562becffe87aa661c1e4a6dbc305d38ec5dc088a', - '7cf92e6458aca7b32edae818f9c2c98c37e06bf72ae0ce80649a38655ee1e27d', - '34d9421d940b16732f24b94023e9d572a7f9ab8023434a4feb532d2adfc8c2c2', - '158785d1bd04eb99df2e86c54bc13e139862897217400def5d72c280222c4cba', - 'ee7261831e1550dbb8fa82853e9fe506fc5fda3f7b919d8fe74b6282f92763ce', - 'f8e625f977af7c8619c32a369b832bc2d051ecd9c73c51e76370ceabd4f25097', - 'c256597fa898d404ed53425de608ac6bfe426f6e2bb457f1c554866eb69dcb8d', - '6bf6f880e9a59b3cd053e6c7060eeacaacf4dac6697dac20e4bd3f38a2ea2543', - 'd1ab7953e3430790a9f81e1c67f5b58c825acf46bd02848384eebe9af917274c', - 'dfbb1a28a5d58a23a17977def0de10d644258d9c54f886d47d293a411cb62261', + "ba412a0d1480e370173072c9562becffe87aa661c1e4a6dbc305d38ec5dc088a", + "7cf92e6458aca7b32edae818f9c2c98c37e06bf72ae0ce80649a38655ee1e27d", + "34d9421d940b16732f24b94023e9d572a7f9ab8023434a4feb532d2adfc8c2c2", + "158785d1bd04eb99df2e86c54bc13e139862897217400def5d72c280222c4cba", + "ee7261831e1550dbb8fa82853e9fe506fc5fda3f7b919d8fe74b6282f92763ce", + "f8e625f977af7c8619c32a369b832bc2d051ecd9c73c51e76370ceabd4f25097", + "c256597fa898d404ed53425de608ac6bfe426f6e2bb457f1c554866eb69dcb8d", + "6bf6f880e9a59b3cd053e6c7060eeacaacf4dac6697dac20e4bd3f38a2ea2543", + "d1ab7953e3430790a9f81e1c67f5b58c825acf46bd02848384eebe9af917274c", + "dfbb1a28a5d58a23a17977def0de10d644258d9c54f886d47d293a411cb62261", ] hashes = [bytes.fromhex(h)[::-1] for h in hex_hashes] self.assertEqual(mb.hashes, hashes) - flags = bytes.fromhex('b55635') + flags = bytes.fromhex("b55635") self.assertEqual(mb.flags, flags) def test_is_valid(self): - hex_merkle_block = '00000020df3b053dc46f162a9b00c7f0d5124e2676d47bbe7c5d0793a500000000000000ef445fef2ed495c275892206ca533e7411907971013ab83e3b47bd0d692d14d4dc7c835b67d8001ac157e670bf0d00000aba412a0d1480e370173072c9562becffe87aa661c1e4a6dbc305d38ec5dc088a7cf92e6458aca7b32edae818f9c2c98c37e06bf72ae0ce80649a38655ee1e27d34d9421d940b16732f24b94023e9d572a7f9ab8023434a4feb532d2adfc8c2c2158785d1bd04eb99df2e86c54bc13e139862897217400def5d72c280222c4cbaee7261831e1550dbb8fa82853e9fe506fc5fda3f7b919d8fe74b6282f92763cef8e625f977af7c8619c32a369b832bc2d051ecd9c73c51e76370ceabd4f25097c256597fa898d404ed53425de608ac6bfe426f6e2bb457f1c554866eb69dcb8d6bf6f880e9a59b3cd053e6c7060eeacaacf4dac6697dac20e4bd3f38a2ea2543d1ab7953e3430790a9f81e1c67f5b58c825acf46bd02848384eebe9af917274cdfbb1a28a5d58a23a17977def0de10d644258d9c54f886d47d293a411cb6226103b55635' + hex_merkle_block = "00000020df3b053dc46f162a9b00c7f0d5124e2676d47bbe7c5d0793a500000000000000ef445fef2ed495c275892206ca533e7411907971013ab83e3b47bd0d692d14d4dc7c835b67d8001ac157e670bf0d00000aba412a0d1480e370173072c9562becffe87aa661c1e4a6dbc305d38ec5dc088a7cf92e6458aca7b32edae818f9c2c98c37e06bf72ae0ce80649a38655ee1e27d34d9421d940b16732f24b94023e9d572a7f9ab8023434a4feb532d2adfc8c2c2158785d1bd04eb99df2e86c54bc13e139862897217400def5d72c280222c4cbaee7261831e1550dbb8fa82853e9fe506fc5fda3f7b919d8fe74b6282f92763cef8e625f977af7c8619c32a369b832bc2d051ecd9c73c51e76370ceabd4f25097c256597fa898d404ed53425de608ac6bfe426f6e2bb457f1c554866eb69dcb8d6bf6f880e9a59b3cd053e6c7060eeacaacf4dac6697dac20e4bd3f38a2ea2543d1ab7953e3430790a9f81e1c67f5b58c825acf46bd02848384eebe9af917274cdfbb1a28a5d58a23a17977def0de10d644258d9c54f886d47d293a411cb6226103b55635" mb = MerkleBlock.parse(BytesIO(bytes.fromhex(hex_merkle_block))) self.assertTrue(mb.is_valid()) - want = '6122b61c413a297dd486f8549c8d2544d610def0de7779a1238ad5a5281abbdf' + want = "6122b61c413a297dd486f8549c8d2544d610def0de7779a1238ad5a5281abbdf" self.assertEqual(mb.proved_txs()[0].hex(), want) class MerkleTreeTest(TestCase): - def test_init(self): tree = MerkleTree(9) self.assertEqual(len(tree.nodes[0]), 1) @@ -179,19 +224,19 @@ def test_populate_tree_1(self): tree = MerkleTree(len(hex_hashes)) hashes = [bytes.fromhex(h) for h in hex_hashes] tree.populate_tree([1] * 31, hashes) - root = '597c4bafe3832b17cbbabe56f878f4fc2ad0f6a402cee7fa851a9cb205f87ed1' + root = "597c4bafe3832b17cbbabe56f878f4fc2ad0f6a402cee7fa851a9cb205f87ed1" self.assertEqual(tree.root().hex(), root) def test_populate_tree_2(self): hex_hashes = [ - '42f6f52f17620653dcc909e58bb352e0bd4bd1381e2955d19c00959a22122b2e', - '94c3af34b9667bf787e1c6a0a009201589755d01d02fe2877cc69b929d2418d4', - '959428d7c48113cb9149d0566bde3d46e98cf028053c522b8fa8f735241aa953', - 'a9f27b99d5d108dede755710d4a1ffa2c74af70b4ca71726fa57d68454e609a2', - '62af110031e29de1efcad103b3ad4bec7bdcf6cb9c9f4afdd586981795516577', + "42f6f52f17620653dcc909e58bb352e0bd4bd1381e2955d19c00959a22122b2e", + "94c3af34b9667bf787e1c6a0a009201589755d01d02fe2877cc69b929d2418d4", + "959428d7c48113cb9149d0566bde3d46e98cf028053c522b8fa8f735241aa953", + "a9f27b99d5d108dede755710d4a1ffa2c74af70b4ca71726fa57d68454e609a2", + "62af110031e29de1efcad103b3ad4bec7bdcf6cb9c9f4afdd586981795516577", ] tree = MerkleTree(len(hex_hashes)) hashes = [bytes.fromhex(h) for h in hex_hashes] tree.populate_tree([1] * 11, hashes) - root = 'a8e8bd023169b81bc56854137a135b97ef47a6a7237f4c6e037baed16285a5ab' + root = "a8e8bd023169b81bc56854137a135b97ef47a6a7237f4c6e037baed16285a5ab" self.assertEqual(tree.root().hex(), root) diff --git a/tests/test_rpc.py b/tests/test_rpc.py index 2ce68773e9..bbe533d1ca 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -2,9 +2,15 @@ from cryptoadvance.specter.rpc import BitcoinRPC, RpcError + def test_BitcoinRpc(bitcoin_regtest): - brt = bitcoin_regtest # stupid long name - rpc = BitcoinRPC(brt.rpcconn.rpcuser, brt.rpcconn.rpcpassword, host=brt.rpcconn.ipaddress, port=brt.rpcconn.rpcport) + brt = bitcoin_regtest # stupid long name + rpc = BitcoinRPC( + brt.rpcconn.rpcuser, + brt.rpcconn.rpcpassword, + host=brt.rpcconn.ipaddress, + port=brt.rpcconn.rpcport, + ) rpc.getblockchaininfo() # To investigate the Bitcoin API, here are some great resources: # https://bitcoin.org/en/developer-reference#bitcoin-core-apis @@ -17,7 +23,3 @@ def test_BitcoinRpc(bitcoin_regtest): except RpcError as rpce: assert rpce.error_code == -32601 assert rpce.error_msg == "Method not found" - - - - diff --git a/tests/test_specter.py b/tests/test_specter.py index b359e752da..0d032d9e8c 100644 --- a/tests/test_specter.py +++ b/tests/test_specter.py @@ -9,6 +9,7 @@ def test_alias(): assert alias("wurst_1") == "wurst_1" assert alias("Wurst$ 1") == "wurst_1" + @pytest.mark.skip(reason="no idea why this does not pass on gitlab exclusively") def test_get_rpc(specter_regtest_configured): specter_regtest_configured.check() @@ -16,24 +17,25 @@ def test_get_rpc(specter_regtest_configured): "autodetect": False, "user": "bitcoin", "password": "secret", - "port": specter_regtest_configured.config['rpc']['port'], + "port": specter_regtest_configured.config["rpc"]["port"], "host": "localhost", - "protocol": "http" + "protocol": "http", } print("rpc_config_data: {}".format(rpc_config_data)) rpc = get_rpc(rpc_config_data) - assert rpc.getblockchaininfo() + assert rpc.getblockchaininfo() assert isinstance(rpc, BitcoinRPC) # ToDo test autodetection-features -def test_specter(specter_regtest_configured,caplog): + +def test_specter(specter_regtest_configured, caplog): caplog.set_level(logging.DEBUG) specter_regtest_configured.check() assert specter_regtest_configured.wallet_manager is not None assert specter_regtest_configured.device_manager is not None - assert specter_regtest_configured.config['rpc']['host'] != "None" - logging.debug("out {}".format(specter_regtest_configured.test_rpc() )) - json_return = json.loads(specter_regtest_configured.test_rpc()["out"] ) + assert specter_regtest_configured.config["rpc"]["host"] != "None" + logging.debug("out {}".format(specter_regtest_configured.test_rpc())) + json_return = json.loads(specter_regtest_configured.test_rpc()["out"]) # that might only work if your chain is fresh # assert json_return['blocks'] == 100 - assert json_return['chain'] == 'regtest' + assert json_return["chain"] == "regtest" diff --git a/tests/test_wallet_manager.py b/tests/test_wallet_manager.py index b01d128f07..1cca52e16c 100644 --- a/tests/test_wallet_manager.py +++ b/tests/test_wallet_manager.py @@ -7,17 +7,19 @@ def test_WalletManager(bitcoin_regtest, devices_filled_data_folder, device_manager): - wm = WalletManager(devices_filled_data_folder,bitcoin_regtest.get_rpc(), "regtest", device_manager) + wm = WalletManager( + devices_filled_data_folder, bitcoin_regtest.get_rpc(), "regtest", device_manager + ) # A wallet-creation needs a device - device = device_manager.get_by_alias('trezor') + device = device_manager.get_by_alias("trezor") assert device != None # Lets's create a wallet with the WalletManager - wm.create_wallet('a_test_wallet', 1, 'wpkh', [device.keys[5]], [device]) + wm.create_wallet("a_test_wallet", 1, "wpkh", [device.keys[5]], [device]) # The wallet-name gets its filename and therefore its alias - wallet = wm.wallets['a_test_wallet'] + wallet = wm.wallets["a_test_wallet"] assert wallet != None - assert wallet.balance['trusted'] == 0 - assert wallet.balance['untrusted_pending'] == 0 + assert wallet.balance["trusted"] == 0 + assert wallet.balance["untrusted_pending"] == 0 # this is a sum of both assert wallet.fullbalance == 0 address = wallet.getnewaddress() @@ -29,10 +31,16 @@ def test_WalletManager(bitcoin_regtest, devices_filled_data_folder, device_manag # update the balance wallet.get_balance() assert wallet.fullbalance >= 25 - + # You can create a multisig wallet with the wallet manager like this - second_device = device_manager.get_by_alias('specter') - multisig_wallet = wm.create_wallet('a_multisig_test_wallet', 1, 'wsh', [device.keys[7], second_device.keys[0]], [device, second_device]) + second_device = device_manager.get_by_alias("specter") + multisig_wallet = wm.create_wallet( + "a_multisig_test_wallet", + 1, + "wsh", + [device.keys[7], second_device.keys[0]], + [device, second_device], + ) assert len(wm.wallets) == 2 assert multisig_wallet != None @@ -44,13 +52,13 @@ def test_WalletManager(bitcoin_regtest, devices_filled_data_folder, device_manag multisig_wallet.get_balance() assert multisig_wallet.fullbalance >= 12.5 # The WalletManager also has a `wallets_names` property, returning a sorted list of the names of all wallets - assert wm.wallets_names == ['a_multisig_test_wallet', 'a_test_wallet'] + assert wm.wallets_names == ["a_multisig_test_wallet", "a_test_wallet"] # You can rename a wallet using the wallet manager using `rename_wallet`, passing the wallet object and the new name to assign to it - wm.rename_wallet(multisig_wallet, 'new_name_test_wallet') - assert multisig_wallet.name == 'new_name_test_wallet' - assert wm.wallets_names == ['a_test_wallet', 'new_name_test_wallet'] - + wm.rename_wallet(multisig_wallet, "new_name_test_wallet") + assert multisig_wallet.name == "new_name_test_wallet" + assert wm.wallets_names == ["a_test_wallet", "new_name_test_wallet"] + # you can also delete a wallet by passing it to the wallet manager's `delete_wallet` method # it will delete the json and attempt to remove it from Bitcoin Core wallet_fullpath = multisig_wallet.fullpath @@ -59,21 +67,26 @@ def test_WalletManager(bitcoin_regtest, devices_filled_data_folder, device_manag assert not os.path.exists(wallet_fullpath) assert len(wm.wallets) == 1 + def test_wallet_createpsbt(bitcoin_regtest, devices_filled_data_folder, device_manager): - wm = WalletManager(devices_filled_data_folder, bitcoin_regtest.get_rpc(), "regtest", device_manager) + wm = WalletManager( + devices_filled_data_folder, bitcoin_regtest.get_rpc(), "regtest", device_manager + ) # A wallet-creation needs a device - device = device_manager.get_by_alias('specter') - key = Key.from_json({ - "derivation": "m/48h/1h/0h/2h", - "original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", - "fingerprint": "08686ac6", - "type": "wsh", - "xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL" - }) - wallet = wm.create_wallet('a_second_test_wallet', 1, 'wpkh', [key], [device]) + device = device_manager.get_by_alias("specter") + key = Key.from_json( + { + "derivation": "m/48h/1h/0h/2h", + "original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", + "fingerprint": "08686ac6", + "type": "wsh", + "xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL", + } + ) + wallet = wm.create_wallet("a_second_test_wallet", 1, "wpkh", [key], [device]) # Let's fund the wallet with ... let's say 40 blocks a 50 coins each --> 200 coins address = wallet.getnewaddress() - assert address == 'bcrt1qtnrv2jpygx2ef3zqfjhqplnycxak2m6ljnhq6z' + assert address == "bcrt1qtnrv2jpygx2ef3zqfjhqplnycxak2m6ljnhq6z" wallet.rpc.generatetoaddress(20, address) # in two addresses address = wallet.getnewaddress() @@ -91,87 +104,140 @@ def test_wallet_createpsbt(bitcoin_regtest, devices_filled_data_folder, device_m unspents = wallet.rpc.listunspent(0) # Lets take 3 more or less random txs from the unspents: selected_coins = [ - "{},{},{}".format(unspents[5]['txid'], unspents[5]['vout'], unspents[5]['amount']), - "{},{},{}".format(unspents[9]['txid'], unspents[9]['vout'], unspents[9]['amount']), - "{},{},{}".format(unspents[12]['txid'], unspents[12]['vout'], unspents[12]['amount']) + "{},{},{}".format( + unspents[5]["txid"], unspents[5]["vout"], unspents[5]["amount"] + ), + "{},{},{}".format( + unspents[9]["txid"], unspents[9]["vout"], unspents[9]["amount"] + ), + "{},{},{}".format( + unspents[12]["txid"], unspents[12]["vout"], unspents[12]["amount"] + ), ] - selected_coins_amount_sum = unspents[5]['amount'] + unspents[9]['amount'] + unspents[12]['amount'] - number_of_coins_to_spend = selected_coins_amount_sum - 0.1 # Let's spend almost all of them - psbt = wallet.createpsbt([random_address], [number_of_coins_to_spend], True, 0, 10, selected_coins=selected_coins) - assert len(psbt['tx']['vin']) == 3 - psbt_txs = [ tx['txid'] for tx in psbt['tx']['vin'] ] + selected_coins_amount_sum = ( + unspents[5]["amount"] + unspents[9]["amount"] + unspents[12]["amount"] + ) + number_of_coins_to_spend = ( + selected_coins_amount_sum - 0.1 + ) # Let's spend almost all of them + psbt = wallet.createpsbt( + [random_address], + [number_of_coins_to_spend], + True, + 0, + 10, + selected_coins=selected_coins, + ) + assert len(psbt["tx"]["vin"]) == 3 + psbt_txs = [tx["txid"] for tx in psbt["tx"]["vin"]] for coin in selected_coins: assert coin.split(",")[0] in psbt_txs # Now let's spend more coins than we have selected. This should result in an exception: try: - psbt = wallet.createpsbt([random_address], [number_of_coins_to_spend +1], True, 0, 10, selected_coins=selected_coins) + psbt = wallet.createpsbt( + [random_address], + [number_of_coins_to_spend + 1], + True, + 0, + 10, + selected_coins=selected_coins, + ) assert False, "should throw an exception!" except SpecterError as e: pass assert wallet.locked_amount == selected_coins_amount_sum assert len(wallet.rpc.listlockunspent()) == 3 - assert wallet.full_available_balance == wallet.fullbalance - selected_coins_amount_sum + assert ( + wallet.full_available_balance == wallet.fullbalance - selected_coins_amount_sum + ) - wallet.delete_pending_psbt(psbt['tx']['txid']) + wallet.delete_pending_psbt(psbt["tx"]["txid"]) assert wallet.locked_amount == 0 assert len(wallet.rpc.listlockunspent()) == 0 assert wallet.full_available_balance == wallet.fullbalance -def test_wallet_sortedmulti(bitcoin_regtest, devices_filled_data_folder, device_manager): - wm = WalletManager(devices_filled_data_folder, bitcoin_regtest.get_rpc(), "regtest", device_manager) - device = device_manager.get_by_alias('trezor') - second_device = device_manager.get_by_alias('specter') + +def test_wallet_sortedmulti( + bitcoin_regtest, devices_filled_data_folder, device_manager +): + wm = WalletManager( + devices_filled_data_folder, bitcoin_regtest.get_rpc(), "regtest", device_manager + ) + device = device_manager.get_by_alias("trezor") + second_device = device_manager.get_by_alias("specter") for i in range(2): if i == 0: - multisig_wallet = wm.create_wallet('a_multisig_test_wallet', 1, 'wsh', [device.keys[7], second_device.keys[0]], [device, second_device]) + multisig_wallet = wm.create_wallet( + "a_multisig_test_wallet", + 1, + "wsh", + [device.keys[7], second_device.keys[0]], + [device, second_device], + ) else: - multisig_wallet = wm.create_wallet('a_multisig_test_wallet', 1, 'wsh', [second_device.keys[0], device.keys[7]], [second_device, device]) + multisig_wallet = wm.create_wallet( + "a_multisig_test_wallet", + 1, + "wsh", + [second_device.keys[0], device.keys[7]], + [second_device, device], + ) address = multisig_wallet.address address_info = multisig_wallet.rpc.getaddressinfo(address) - assert address_info['pubkeys'][0] < address_info['pubkeys'][1] - + assert address_info["pubkeys"][0] < address_info["pubkeys"][1] + another_address = multisig_wallet.getnewaddress() another_address_info = multisig_wallet.rpc.getaddressinfo(another_address) - assert another_address_info['pubkeys'][0] < another_address_info['pubkeys'][1] - + assert another_address_info["pubkeys"][0] < another_address_info["pubkeys"][1] + third_address = multisig_wallet.get_address(30) third_address_info = multisig_wallet.rpc.getaddressinfo(third_address) - assert third_address_info['pubkeys'][0] < third_address_info['pubkeys'][1] + assert third_address_info["pubkeys"][0] < third_address_info["pubkeys"][1] change_address = multisig_wallet.change_address change_address_info = multisig_wallet.rpc.getaddressinfo(change_address) - assert change_address_info['pubkeys'][0] < change_address_info['pubkeys'][1] + assert change_address_info["pubkeys"][0] < change_address_info["pubkeys"][1] another_change_address = multisig_wallet.get_address(30, change=True) - another_change_address_info = multisig_wallet.rpc.getaddressinfo(another_change_address) - assert another_change_address_info['pubkeys'][0] < another_change_address_info['pubkeys'][1] + another_change_address_info = multisig_wallet.rpc.getaddressinfo( + another_change_address + ) + assert ( + another_change_address_info["pubkeys"][0] + < another_change_address_info["pubkeys"][1] + ) + def test_wallet_labeling(bitcoin_regtest, devices_filled_data_folder, device_manager): - wm = WalletManager(devices_filled_data_folder, bitcoin_regtest.get_rpc(), "regtest", device_manager) + wm = WalletManager( + devices_filled_data_folder, bitcoin_regtest.get_rpc(), "regtest", device_manager + ) # A wallet-creation needs a device - device = device_manager.get_by_alias('specter') - key = Key.from_json({ - "derivation": "m/48h/1h/0h/2h", - "original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", - "fingerprint": "08686ac6", - "type": "wsh", - "xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL" - }) - wallet = wm.create_wallet('a_second_test_wallet', 1, 'wpkh', [key], [device]) + device = device_manager.get_by_alias("specter") + key = Key.from_json( + { + "derivation": "m/48h/1h/0h/2h", + "original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", + "fingerprint": "08686ac6", + "type": "wsh", + "xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL", + } + ) + wallet = wm.create_wallet("a_second_test_wallet", 1, "wpkh", [key], [device]) address = wallet.address - assert wallet.getlabel(address) == 'Address #0' - wallet.setlabel(address, 'Random label') - assert wallet.getlabel(address) == 'Random label' + assert wallet.getlabel(address) == "Address #0" + wallet.setlabel(address, "Random label") + assert wallet.getlabel(address) == "Random label" wallet.rpc.generatetoaddress(20, address) random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r" wallet.rpc.generatetoaddress(100, random_address) - + # update utxo wallet.getdata() # update balance @@ -181,14 +247,14 @@ def test_wallet_labeling(bitcoin_regtest, devices_filled_data_folder, device_man assert len(wallet.utxo) == 20 assert wallet.is_current_address_used assert wallet.balance_on_address(address) == address_balance - assert wallet.balance_on_label('Random label') == address_balance - assert wallet.addresses_on_label('Random label') == [address] + assert wallet.balance_on_label("Random label") == address_balance + assert wallet.addresses_on_label("Random label") == [address] assert wallet.utxo_addresses == [address] - assert wallet.utxo_labels == ['Random label'] + assert wallet.utxo_labels == ["Random label"] assert wallet.utxo_addresses == [address] new_address = wallet.getnewaddress() - wallet.setlabel(new_address, '') + wallet.setlabel(new_address, "") wallet.rpc.generatetoaddress(20, new_address) random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r" @@ -200,46 +266,57 @@ def test_wallet_labeling(bitcoin_regtest, devices_filled_data_folder, device_man assert len(wallet.utxo) == 40 assert wallet.is_current_address_used assert wallet.utxo_on_address(address) == 20 - assert wallet.balance_on_address(new_address) == wallet.fullbalance - address_balance + assert ( + wallet.balance_on_address(new_address) == wallet.fullbalance - address_balance + ) assert sorted(wallet.utxo_addresses) == sorted([address, new_address]) - assert sorted(wallet.utxo_labels) == sorted(['Random label', new_address]) + assert sorted(wallet.utxo_labels) == sorted(["Random label", new_address]) assert sorted(wallet.utxo_addresses) == sorted([address, new_address]) assert wallet.get_address_name(new_address, -1) == new_address - assert wallet.get_address_name(new_address, 5) == 'Address #5' - assert wallet.get_address_name(address, 5) == 'Random label' + assert wallet.get_address_name(new_address, 5) == "Address #5" + assert wallet.get_address_name(address, 5) == "Random label" - wallet.setlabel(new_address, '') + wallet.setlabel(new_address, "") third_address = wallet.getnewaddress() wallet.getdata() - assert sorted(wallet.labels) == sorted(['Random label', new_address, 'Address #2']) - assert sorted(wallet.utxo_labels) == sorted(['Random label', new_address]) + assert sorted(wallet.labels) == sorted(["Random label", new_address, "Address #2"]) + assert sorted(wallet.utxo_labels) == sorted(["Random label", new_address]) assert sorted(wallet.addresses) == sorted([address, new_address, third_address]) assert sorted(wallet.utxo_addresses) == sorted([address, new_address]) - wallet.setlabel(third_address, 'Random label') + wallet.setlabel(third_address, "Random label") wallet.getdata() - assert sorted(wallet.addresses_on_label('Random label')) == sorted([address, third_address]) + assert sorted(wallet.addresses_on_label("Random label")) == sorted( + [address, third_address] + ) -def test_wallet_change_addresses(bitcoin_regtest, devices_filled_data_folder, device_manager): - wm = WalletManager(devices_filled_data_folder, bitcoin_regtest.get_rpc(), "regtest", device_manager) + +def test_wallet_change_addresses( + bitcoin_regtest, devices_filled_data_folder, device_manager +): + wm = WalletManager( + devices_filled_data_folder, bitcoin_regtest.get_rpc(), "regtest", device_manager + ) # A wallet-creation needs a device - device = device_manager.get_by_alias('specter') - key = Key.from_json({ - "derivation": "m/48h/1h/0h/2h", - "original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", - "fingerprint": "08686ac6", - "type": "wsh", - "xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL" - }) - wallet = wm.create_wallet('a_second_test_wallet', 1, 'wpkh', [key], [device]) + device = device_manager.get_by_alias("specter") + key = Key.from_json( + { + "derivation": "m/48h/1h/0h/2h", + "original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", + "fingerprint": "08686ac6", + "type": "wsh", + "xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL", + } + ) + wallet = wm.create_wallet("a_second_test_wallet", 1, "wpkh", [key], [device]) address = wallet.address change_address = wallet.change_address assert wallet.addresses == [address] assert wallet.change_addresses == [change_address] assert wallet.active_addresses == [address] - assert wallet.labels == ['Address #0'] + assert wallet.labels == ["Address #0"] wallet.rpc.generatetoaddress(20, change_address) random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r" @@ -253,6 +330,7 @@ def test_wallet_change_addresses(bitcoin_regtest, devices_filled_data_folder, de assert wallet.active_addresses == [address, change_address] # labels should return only active addresses - assert wallet.labels == ['Address #0', 'Change #0'] + assert wallet.labels == ["Address #0", "Change #0"] + # TODO: Add more tests of the Wallet object