mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
blacklify everything (#464)
This commit is contained in:
parent
318889bdca
commit
5702559317
56 changed files with 3493 additions and 2425 deletions
|
|
@ -1,4 +1,5 @@
|
|||
from hwilib.devices import __all__
|
||||
|
||||
hiddenimports = []
|
||||
for d in __all__:
|
||||
hiddenimports.append('hwilib.devices.' + d)
|
||||
hiddenimports.append("hwilib.devices." + d)
|
||||
|
|
|
|||
|
|
@ -5,21 +5,24 @@ import sys
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -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 @@ running = True
|
|||
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 @@ class SpecterPreferencesDialog(QDialog):
|
|||
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 @@ class SpecterPreferencesDialog(QDialog):
|
|||
|
||||
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 @@ class ProcessRunnable(QRunnable):
|
|||
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 @@ class ProcessRunnable(QRunnable):
|
|||
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 @@ class WebEnginePage(QWebEnginePage):
|
|||
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 @@ class WebEnginePage(QWebEnginePage):
|
|||
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 @@ class WebView(QWidget):
|
|||
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 @@ class WebView(QWidget):
|
|||
|
||||
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 @@ class WebView(QWidget):
|
|||
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 @@ class Application(QApplication):
|
|||
quit_specter(self)
|
||||
return False
|
||||
|
||||
|
||||
def init_desktop_app():
|
||||
app = Application([])
|
||||
app.setQuitOnLastWindowClosed(False)
|
||||
|
|
@ -390,16 +416,18 @@ def init_desktop_app():
|
|||
# So maybe let's keep it in here.
|
||||
if os.environ.get("DEP_REPORTING"):
|
||||
import psutil
|
||||
print("---------------------------DEP_REPORTING--------------------------------------------")
|
||||
|
||||
print(
|
||||
"---------------------------DEP_REPORTING--------------------------------------------"
|
||||
)
|
||||
for item in psutil.Process().memory_maps():
|
||||
print(item.path)
|
||||
print("-----------------------------DEP_REPORTING(end)-------------------------------------")
|
||||
|
||||
print(
|
||||
"-----------------------------DEP_REPORTING(end)-------------------------------------"
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
|
@ -410,11 +438,13 @@ def init_desktop_app():
|
|||
|
||||
# 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)
|
||||
|
||||
|
|
@ -443,11 +473,11 @@ def init_desktop_app():
|
|||
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
|
||||
|
|
|
|||
|
|
@ -6,15 +6,17 @@ import logging
|
|||
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)
|
||||
|
|
|
|||
12
setup.py
12
setup.py
|
|
@ -2,11 +2,11 @@ from glob import glob
|
|||
|
||||
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 @@ setup(
|
|||
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 @@ setup(
|
|||
"Operating System :: OS Independent",
|
||||
"Framework :: Flask",
|
||||
],
|
||||
python_requires='>=3.6',
|
||||
python_requires=">=3.6",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ if __name__ == "__main__":
|
|||
# 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)
|
||||
|
|
|
|||
|
|
@ -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 @@ from .helpers import load_jsons
|
|||
|
||||
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 @@ class Btcd_conn:
|
|||
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 '<Btcd_conn {}>'.format(self.render_url())
|
||||
return "<Btcd_conn {}>".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 @@ class BitcoindController:
|
|||
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 @@ class BitcoindController:
|
|||
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 @@ class BitcoindController:
|
|||
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 @@ class BitcoindController:
|
|||
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 @@ class BitcoindController:
|
|||
|
||||
@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 @@ class BitcoindController:
|
|||
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 @@ class BitcoindDockerController(BitcoindController):
|
|||
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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 server(daemon, stop, restart, force,
|
|||
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 server(daemon, stop, restart, force,
|
|||
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 server(daemon, stop, restart, force,
|
|||
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 \
|
||||
registry.gitlab.com/cryptoadvance/specter-desktop\
|
||||
/python-bitcoind:{docker_tag}")
|
||||
sys.exit(1)
|
||||
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)
|
||||
f" --> Try to download first with docker pull \
|
||||
registry.gitlab.com/cryptoadvance/specter-desktop\
|
||||
/python-bitcoind:{docker_tag}"
|
||||
)
|
||||
sys.exit(1)
|
||||
click.echo(" --> containerImage: %s" %
|
||||
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
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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 @@ DATA_FOLDER = "~/.specter"
|
|||
# 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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -35,16 +35,13 @@ class Device:
|
|||
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 @@ class Device:
|
|||
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 @@ class Device:
|
|||
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):
|
||||
|
|
|
|||
|
|
@ -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 @@ class DeviceManager:
|
|||
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 @@ class DeviceManager:
|
|||
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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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 @@ class BitcoinCore(Device):
|
|||
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 @@ class BitcoinCore(Device):
|
|||
# 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 @@ class BitcoinCore(Device):
|
|||
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 @@ class BitcoinCore(Device):
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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 @@ class Cobo(SDCardDevice):
|
|||
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 @@ Name: {}
|
|||
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())
|
||||
|
|
|
|||
|
|
@ -3,11 +3,7 @@ from .sd_card_device import SDCardDevice
|
|||
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 @@ class ColdCard(SDCardDevice):
|
|||
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 @@ Name: {}
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ class Electrum(Device):
|
|||
# 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
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class GenericDevice(Device):
|
|||
|
||||
def create_psbts(self, base64_psbt, wallet):
|
||||
psbts = {
|
||||
'qrcode': base64_psbt,
|
||||
'sdcard': base64_psbt,
|
||||
"qrcode": base64_psbt,
|
||||
"sdcard": base64_psbt,
|
||||
}
|
||||
return psbts
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
self.type = "Keepkey"
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@ from typing import Dict, Optional, Union
|
|||
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
|
||||
from binascii import b2a_base64
|
||||
|
|
@ -22,14 +26,13 @@ class SpecterClient(HardwareWalletClient):
|
|||
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:
|
||||
|
|
@ -60,9 +63,9 @@ class SpecterClient(HardwareWalletClient):
|
|||
# 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).
|
||||
|
|
@ -71,7 +74,7 @@ class SpecterClient(HardwareWalletClient):
|
|||
"""
|
||||
# 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: Union[str, bytes], bip32_path: str
|
||||
|
|
@ -127,7 +130,7 @@ class SpecterClient(HardwareWalletClient):
|
|||
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.
|
||||
|
|
@ -138,8 +141,9 @@ class SpecterClient(HardwareWalletClient):
|
|||
|
||||
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 = ""
|
||||
|
|
@ -152,8 +156,9 @@ class SpecterClient(HardwareWalletClient):
|
|||
|
||||
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
|
||||
|
|
@ -166,8 +171,9 @@ class SpecterClient(HardwareWalletClient):
|
|||
|
||||
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 = ""
|
||||
|
|
@ -180,8 +186,9 @@ class SpecterClient(HardwareWalletClient):
|
|||
|
||||
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."""
|
||||
|
|
@ -197,8 +204,9 @@ class SpecterClient(HardwareWalletClient):
|
|||
|
||||
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.
|
||||
|
|
@ -209,8 +217,9 @@ class SpecterClient(HardwareWalletClient):
|
|||
|
||||
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.
|
||||
|
|
@ -221,15 +230,15 @@ class SpecterClient(HardwareWalletClient):
|
|||
|
||||
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):
|
||||
if num_bytes < 0 or num_bytes > 10000:
|
||||
raise BadArgumentError(
|
||||
"We can only get up to 10k bytes of random data")
|
||||
raise BadArgumentError("We can only get up to 10k bytes of random data")
|
||||
res = self.query("getrandom %d" % num_bytes)
|
||||
return bytes.fromhex(res)
|
||||
|
||||
|
|
@ -238,16 +247,18 @@ class SpecterClient(HardwareWalletClient):
|
|||
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
|
||||
|
|
@ -263,25 +274,26 @@ 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)
|
||||
|
||||
|
|
@ -292,6 +304,7 @@ def is_micropython(port):
|
|||
|
||||
class SpecterBase:
|
||||
"""Class with common constants and command encoding"""
|
||||
|
||||
EOL = b"\r\n"
|
||||
ACK = b"ACK"
|
||||
ACK_TIMOUT = 1
|
||||
|
|
@ -302,7 +315,7 @@ class SpecterBase:
|
|||
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):
|
||||
|
|
@ -324,7 +337,7 @@ class SpecterUSBDevice(SpecterBase):
|
|||
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
|
||||
|
|
@ -335,12 +348,12 @@ class SpecterUSBDevice(SpecterBase):
|
|||
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()
|
||||
|
||||
|
|
@ -364,7 +377,7 @@ class SpecterSimulator(SpecterBase):
|
|||
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
|
||||
|
|
@ -375,10 +388,10 @@ class SpecterSimulator(SpecterBase):
|
|||
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()
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ import logging
|
|||
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 trezor_exception(f):
|
|||
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 @@ class TrezorClient(HardwareWalletClient):
|
|||
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 @@ class TrezorClient(HardwareWalletClient):
|
|||
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 @@ class TrezorClient(HardwareWalletClient):
|
|||
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 @@ class TrezorClient(HardwareWalletClient):
|
|||
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 @@ class TrezorClient(HardwareWalletClient):
|
|||
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 @@ class TrezorClient(HardwareWalletClient):
|
|||
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 @@ class TrezorClient(HardwareWalletClient):
|
|||
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 @@ class TrezorClient(HardwareWalletClient):
|
|||
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 @@ class TrezorClient(HardwareWalletClient):
|
|||
|
||||
# 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 @@ class TrezorClient(HardwareWalletClient):
|
|||
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 @@ class TrezorClient(HardwareWalletClient):
|
|||
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 @@ class TrezorClient(HardwareWalletClient):
|
|||
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 @@ class TrezorClient(HardwareWalletClient):
|
|||
# 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 @@ class TrezorClient(HardwareWalletClient):
|
|||
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 @@ class TrezorClient(HardwareWalletClient):
|
|||
script_type=script_type,
|
||||
multisig=multisig,
|
||||
)
|
||||
return {'address': address}
|
||||
return {"address": address}
|
||||
except:
|
||||
pass
|
||||
|
||||
|
|
@ -495,38 +563,48 @@ class TrezorClient(HardwareWalletClient):
|
|||
|
||||
# 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 @@ class TrezorClient(HardwareWalletClient):
|
|||
# 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 @@ class TrezorClient(HardwareWalletClient):
|
|||
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["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["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
|
||||
return results
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from ..device import Device
|
|||
import hwilib.commands as hwi_commands
|
||||
import importlib
|
||||
|
||||
|
||||
class HWIDevice(Device):
|
||||
|
||||
hwi_support = True
|
||||
|
|
@ -11,7 +12,7 @@ class HWIDevice(Device):
|
|||
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):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from .hwi_device import HWIDevice
|
||||
|
||||
# a hack that verifies multisig
|
||||
from .hwi import keepkey
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,5 @@ class SDCardDevice(HWIDevice):
|
|||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from .hwi_device import HWIDevice
|
|||
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 @@ class Specter(HWIDevice):
|
|||
# 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 @@ class Specter(HWIDevice):
|
|||
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]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -23,18 +23,22 @@ logger = logging.getLogger(__name__)
|
|||
# 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,49 +124,42 @@ def which(program):
|
|||
# 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 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
|
||||
|
|
@ -173,35 +169,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
|
||||
|
||||
|
||||
|
|
@ -212,8 +210,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
|
||||
|
||||
|
|
@ -229,12 +231,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(",")
|
||||
|
|
@ -245,9 +250,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
|
||||
|
|
@ -283,16 +288,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
|
||||
|
||||
|
||||
|
|
@ -307,17 +312,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
|
||||
|
|
|
|||
|
|
@ -11,22 +11,25 @@ import logging
|
|||
|
||||
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 @@ class HWIBridge(JSONRPC):
|
|||
"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.
|
||||
|
|
@ -68,7 +71,8 @@ class HWIBridge(JSONRPC):
|
|||
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
|
||||
):
|
||||
|
|
@ -76,7 +80,7 @@ class HWIBridge(JSONRPC):
|
|||
client = None
|
||||
try:
|
||||
client = devcls.get_client(dev["path"], passphrase)
|
||||
dev['fingerprint'] = client.get_master_fingerprint_hex()
|
||||
dev["fingerprint"] = client.get_master_fingerprint_hex()
|
||||
finally:
|
||||
if client is not None:
|
||||
client.close()
|
||||
|
|
@ -87,9 +91,11 @@ class HWIBridge(JSONRPC):
|
|||
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.
|
||||
|
|
@ -99,27 +105,37 @@ class HWIBridge(JSONRPC):
|
|||
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
|
||||
|
|
@ -127,117 +143,170 @@ class HWIBridge(JSONRPC):
|
|||
# 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:
|
||||
|
|
@ -256,102 +325,78 @@ class HWIBridge(JSONRPC):
|
|||
|
||||
# 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()
|
||||
|
|
|
|||
|
|
@ -6,70 +6,111 @@ from .hwi_rpc import HWIBridge
|
|||
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.<br>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.<br>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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,43 +4,46 @@ from .util.base58 import decode_base58, encode_base58_checksum
|
|||
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 @@ class Key:
|
|||
|
||||
@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 @@ class Key:
|
|||
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 @@ class Key:
|
|||
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 @@ class Key:
|
|||
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 @@ class Key:
|
|||
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 @@ class Key:
|
|||
@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 @@ class Key:
|
|||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -6,31 +6,29 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
# 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, password = content.split(":")
|
||||
obj = {
|
||||
"user": user,
|
||||
"password": password,
|
||||
"port": RPC_PORTS[chain]
|
||||
}
|
||||
obj = {"user": user, "password": password, "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 "password" 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","password":"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","password":"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"),
|
||||
"user": os.getenv("BTC_RPC_USER"),
|
||||
"password": 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
|
||||
"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):
|
|||
logger.info(f"exception: {rpc.host}:{rpc.port} {e}")
|
||||
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,25 +192,28 @@ class RpcError(Exception):
|
|||
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="bitcoin",
|
||||
password="secret",
|
||||
host="127.0.0.1",
|
||||
port=8332,
|
||||
protocol="http",
|
||||
path="",
|
||||
timeout=None,
|
||||
session=None,
|
||||
**kwargs):
|
||||
path = path.replace("//","/") # just in case
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user="bitcoin",
|
||||
password="secret",
|
||||
host="127.0.0.1",
|
||||
port=8332,
|
||||
protocol="http",
|
||||
path="",
|
||||
timeout=None,
|
||||
session=None,
|
||||
**kwargs,
|
||||
):
|
||||
path = path.replace("//", "/") # just in case
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.port = port
|
||||
|
|
@ -221,27 +228,30 @@ class BitcoinRPC:
|
|||
# check if we need to connect over Tor
|
||||
if ".onion" in self.host:
|
||||
# configure Tor proxies
|
||||
session.proxies['http'] = 'socks5h://localhost:9050'
|
||||
session.proxies['https'] = 'socks5h://localhost:9050'
|
||||
session.proxies["http"] = "socks5h://localhost:9050"
|
||||
session.proxies["https"] = "socks5h://localhost:9050"
|
||||
self.session = session
|
||||
|
||||
def wallet(self, name=""):
|
||||
return BitcoinRPC(user=self.user,
|
||||
password=self.password,
|
||||
port=self.port,
|
||||
protocol=self.protocol,
|
||||
host=self.host,
|
||||
path="{}/wallet/{}".format(self.path, name),
|
||||
timeout=self.timeout,
|
||||
session=self.session,
|
||||
return BitcoinRPC(
|
||||
user=self.user,
|
||||
password=self.password,
|
||||
port=self.port,
|
||||
protocol=self.protocol,
|
||||
host=self.host,
|
||||
path="{}/wallet/{}".format(self.path, name),
|
||||
timeout=self.timeout,
|
||||
session=self.session,
|
||||
)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return "{s.protocol}://{s.user}:{s.password}@{s.host}:{s.port}{s.path}".format(s=self)
|
||||
return "{s.protocol}://{s.user}:{s.password}@{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
|
||||
|
|
@ -250,8 +260,8 @@ class BitcoinRPC:
|
|||
|
||||
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,
|
||||
|
|
@ -271,50 +281,47 @@ class BitcoinRPC:
|
|||
# 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 = self.session.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)
|
||||
|
|
@ -329,8 +336,8 @@ if __name__ == '__main__':
|
|||
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from .util.version import VersionChecker
|
|||
|
||||
logger = logging.getLogger()
|
||||
|
||||
env_path = Path('.') / '.flaskenv'
|
||||
env_path = Path(".") / ".flaskenv"
|
||||
load_dotenv(env_path)
|
||||
|
||||
|
||||
|
|
@ -25,26 +25,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:
|
||||
|
|
@ -71,35 +69,39 @@ def init_app(app, hwibridge=False, specter=None):
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import threading
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_rpc(conf, old_rpc=None):
|
||||
"""
|
||||
Checks if config have changed,
|
||||
|
|
@ -27,9 +28,13 @@ def get_rpc(conf, old_rpc=None):
|
|||
rpc = None
|
||||
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:
|
||||
|
|
@ -54,8 +59,10 @@ def get_rpc(conf, old_rpc=None):
|
|||
logger.info("rpc config have changed.")
|
||||
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()
|
||||
|
|
@ -82,16 +89,11 @@ class Specter:
|
|||
"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
|
||||
|
|
@ -110,8 +112,8 @@ class Specter:
|
|||
|
||||
@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):
|
||||
|
|
@ -124,9 +126,9 @@ class Specter:
|
|||
# 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
|
||||
|
|
@ -135,12 +137,13 @@ class Specter:
|
|||
# update rpc if something doesn't work
|
||||
if self.rpc is None or not self.rpc.test_connection():
|
||||
self.rpc = get_rpc(self.config["rpc"], self.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),
|
||||
|
|
@ -153,61 +156,73 @@ class Specter:
|
|||
]
|
||||
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):
|
||||
|
|
@ -228,30 +243,32 @@ class Specter:
|
|||
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)
|
||||
r['tests']['wallets'] = False
|
||||
r["tests"]["wallets"] = False
|
||||
|
||||
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:
|
||||
|
|
@ -283,13 +300,13 @@ class Specter:
|
|||
return self.rpc is not None
|
||||
|
||||
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
|
||||
|
|
@ -297,7 +314,7 @@ class Specter:
|
|||
# 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()
|
||||
|
|
@ -305,14 +322,14 @@ class Specter:
|
|||
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/")
|
||||
|
|
@ -320,42 +337,42 @@ class Specter:
|
|||
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
|
||||
|
|
@ -412,13 +429,7 @@ class Specter:
|
|||
|
||||
@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):
|
||||
|
|
@ -433,7 +444,10 @@ class Specter:
|
|||
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 ""
|
||||
|
|
@ -468,25 +482,22 @@ class Specter:
|
|||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
class SpecterError(Exception):
|
||||
''' A SpecterError contains meaningfull messages which can be passed directly to the user '''
|
||||
pass
|
||||
""" A SpecterError contains meaningfull messages which can be passed directly to the user """
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -7,28 +7,38 @@ from flask_login import UserMixin
|
|||
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 @@ class User(UserMixin):
|
|||
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 @@ class User(UserMixin):
|
|||
@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 @@ class User(UserMixin):
|
|||
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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -4,17 +4,19 @@ import hashlib
|
|||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -2,21 +2,23 @@ import re
|
|||
|
||||
# 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 @@ class Descriptor:
|
|||
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 @@ class Descriptor:
|
|||
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 @@ class Descriptor:
|
|||
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 @@ class Descriptor:
|
|||
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 @@ class Descriptor:
|
|||
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 @@ class Descriptor:
|
|||
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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 @@ class JSONRPC:
|
|||
# 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
|
||||
|
|
|
|||
|
|
@ -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, <endianness>) 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'
|
||||
|
||||
def __init__(self, version, prev_block, merkle_root, timestamp, bits, nonce, tx_hashes=None):
|
||||
class Block:
|
||||
command = b"block"
|
||||
|
||||
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 @@ class Block:
|
|||
|
||||
@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 @@ class Block:
|
|||
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 @@ class Block:
|
|||
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 @@ class Block:
|
|||
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 @@ class Block:
|
|||
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 @@ class Block:
|
|||
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 @@ class MerkleTree:
|
|||
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 @@ class MerkleTree:
|
|||
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 @@ class MerkleTree:
|
|||
# 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 @@ class MerkleBlock:
|
|||
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 @@ class MerkleBlock:
|
|||
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 @@ class MerkleBlock:
|
|||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from stem.control import Controller
|
|||
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...
|
||||
|
|
|
|||
|
|
@ -50,12 +50,14 @@ class VersionChecker:
|
|||
Returns latest = "unknown" if fetch failed.
|
||||
"""
|
||||
version_file = "version.txt"
|
||||
if getattr(sys, 'frozen', False):
|
||||
version_file = os.path.join(sys._MEIPASS, 'version.txt')
|
||||
if getattr(sys, "frozen", False):
|
||||
version_file = os.path.join(sys._MEIPASS, "version.txt")
|
||||
with open(version_file) as f:
|
||||
current = f.read().strip()
|
||||
try:
|
||||
releases = requests.get("https://api.github.com/repos/cryptoadvance/specter-desktop/releases").json()
|
||||
releases = requests.get(
|
||||
"https://api.github.com/repos/cryptoadvance/specter-desktop/releases"
|
||||
).json()
|
||||
latest = "unknown"
|
||||
for release in releases:
|
||||
if release["prerelease"] or release["draft"]:
|
||||
|
|
@ -67,36 +69,39 @@ class VersionChecker:
|
|||
return current, latest
|
||||
|
||||
def get_pip_version(self):
|
||||
latest = str(subprocess.run([
|
||||
sys.executable, '-m', 'pip',
|
||||
'install', f'{self.name}==random'],
|
||||
capture_output=True, text=True))
|
||||
latest = latest[latest.find(
|
||||
'(from versions:')+15:]
|
||||
latest = latest[:latest.find(')')]
|
||||
latest = latest.replace(' ', '').split(',')[-1]
|
||||
latest = str(
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", f"{self.name}==random"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
)
|
||||
latest = latest[latest.find("(from versions:") + 15 :]
|
||||
latest = latest[: latest.find(")")]
|
||||
latest = latest.replace(" ", "").split(",")[-1]
|
||||
|
||||
current = str(subprocess.run([
|
||||
sys.executable, '-m', 'pip',
|
||||
'show', f'{self.name}'],
|
||||
capture_output=True, text=True))
|
||||
current = current[current.find(
|
||||
'Version:')+8:]
|
||||
current = current[:current.find(
|
||||
'\\n')].replace(' ', '')
|
||||
current = str(
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "pip", "show", f"{self.name}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
)
|
||||
current = current[current.find("Version:") + 8 :]
|
||||
current = current[: current.find("\\n")].replace(" ", "")
|
||||
# master?
|
||||
if current == 'vx.y.z-get-replaced-by-release-script':
|
||||
current = 'custom'
|
||||
if current == "vx.y.z-get-replaced-by-release-script":
|
||||
current = "custom"
|
||||
# no need to check upgrades
|
||||
self.running = False
|
||||
return current, latest
|
||||
|
||||
def get_version_info(self):
|
||||
'''
|
||||
"""
|
||||
Returns a triple of the current version
|
||||
of the pip-package cryptoadvance.specter and
|
||||
the latest version and whether you should upgrade.
|
||||
'''
|
||||
"""
|
||||
# check if we have version.txt file
|
||||
# this is the case for binaries
|
||||
current = "unknown"
|
||||
|
|
@ -115,12 +120,13 @@ class VersionChecker:
|
|||
logger.error(exc)
|
||||
|
||||
# check that both current and latest versions match the pattern
|
||||
if (re.search(r"v?([\d+]).([\d+]).([\d+]).*", current) and
|
||||
re.search(r"v?([\d+]).([\d+]).([\d+]).*", latest)):
|
||||
if re.search(r"v?([\d+]).([\d+]).([\d+]).*", current) and re.search(
|
||||
r"v?([\d+]).([\d+]).([\d+]).*", latest
|
||||
):
|
||||
return (
|
||||
current,
|
||||
latest,
|
||||
# check without leading v so v1.2.3 = 1.2.3
|
||||
latest.replace("v","") != current.replace("v","")
|
||||
latest.replace("v", "") != current.replace("v", ""),
|
||||
)
|
||||
return current, latest, False
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from math import ceil
|
|||
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
|
||||
|
|
@ -25,6 +25,7 @@ class Wallet():
|
|||
# minimal fee rate is slightly above 1 sat/vbyte
|
||||
# to avoid rounding errors
|
||||
MIN_FEE_RATE = 1.01
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
|
|
@ -71,9 +72,7 @@ class Wallet():
|
|||
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 = int(sigs_required)
|
||||
self.pending_psbts = pending_psbts
|
||||
self.fullpath = fullpath
|
||||
|
|
@ -83,9 +82,9 @@ class Wallet():
|
|||
)
|
||||
self.last_block = last_block
|
||||
|
||||
if address == '':
|
||||
if address == "":
|
||||
self.getnewaddress()
|
||||
if change_address == '':
|
||||
if change_address == "":
|
||||
self.getnewaddress(change=True)
|
||||
|
||||
self.getdata()
|
||||
|
|
@ -116,14 +115,14 @@ class Wallet():
|
|||
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:
|
||||
|
|
@ -142,57 +141,89 @@ class Wallet():
|
|||
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,
|
||||
|
|
@ -214,8 +245,8 @@ class Wallet():
|
|||
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):
|
||||
|
|
@ -228,7 +259,9 @@ class Wallet():
|
|||
def getdata(self):
|
||||
try:
|
||||
self.utxo = parse_utxo(self, self.rpc.listunspent(0))
|
||||
self.utxo_labels_list = self.getlabels(utxo["address"] for utxo in self.utxo)
|
||||
self.utxo_labels_list = self.getlabels(
|
||||
utxo["address"] for utxo in self.utxo
|
||||
)
|
||||
except Exception:
|
||||
self.utxo = []
|
||||
self.utxo_labels_list = {}
|
||||
|
|
@ -236,14 +269,13 @@ class Wallet():
|
|||
# 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
|
||||
|
|
@ -275,7 +307,7 @@ class Wallet():
|
|||
"fullpath": self.fullpath,
|
||||
"last_block": self.last_block,
|
||||
"blockheight": self.blockheight,
|
||||
"labels": self.export_labels()
|
||||
"labels": self.export_labels(),
|
||||
}
|
||||
|
||||
def save_to_file(self):
|
||||
|
|
@ -292,7 +324,12 @@ class Wallet():
|
|||
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):
|
||||
|
|
@ -310,7 +347,9 @@ class Wallet():
|
|||
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"]
|
||||
|
|
@ -328,7 +367,9 @@ class Wallet():
|
|||
|
||||
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:
|
||||
|
|
@ -336,27 +377,47 @@ class Wallet():
|
|||
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
|
||||
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'] }")
|
||||
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)
|
||||
|
||||
|
|
@ -370,20 +431,23 @@ class Wallet():
|
|||
labels = self.rpc.listlabels()
|
||||
if "" in labels:
|
||||
labels.remove("")
|
||||
res = self.rpc.multi([
|
||||
("getaddressesbylabel", label)
|
||||
for label in labels
|
||||
])
|
||||
return { labels[i]: list(result['result'].keys()) for i, result in enumerate(res) }
|
||||
|
||||
res = self.rpc.multi([("getaddressesbylabel", label) for label in labels])
|
||||
return {
|
||||
labels[i]: list(result["result"].keys()) for i, result in enumerate(res)
|
||||
}
|
||||
|
||||
def import_labels(self, labels):
|
||||
# format:
|
||||
# format:
|
||||
# {
|
||||
# 'label1': ['address1', 'address2'],
|
||||
# 'label2': ['address3', 'address4']
|
||||
# }
|
||||
#
|
||||
rpc_calls = [("setlabel", address, label) for label, addresses in labels.items() for address in addresses]
|
||||
rpc_calls = [
|
||||
("setlabel", address, label)
|
||||
for label, addresses in labels.items()
|
||||
for address in addresses
|
||||
]
|
||||
if rpc_calls:
|
||||
self.rpc.multi(rpc_calls)
|
||||
|
||||
|
|
@ -393,23 +457,27 @@ class Wallet():
|
|||
# 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:
|
||||
|
|
@ -431,40 +499,37 @@ class Wallet():
|
|||
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()
|
||||
|
|
@ -472,27 +537,31 @@ class Wallet():
|
|||
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
|
||||
|
|
@ -503,22 +572,34 @@ class Wallet():
|
|||
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"
|
||||
|
|
@ -547,15 +628,10 @@ class Wallet():
|
|||
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]
|
||||
|
|
@ -566,7 +642,7 @@ class Wallet():
|
|||
or from address belonging to the wallet.
|
||||
"""
|
||||
if address is not None:
|
||||
d = self.rpc.getaddressinfo(address)['desc']
|
||||
d = self.rpc.getaddressinfo(address)["desc"]
|
||||
path = d.split("[")[1].split("]")[0].split("/")
|
||||
change = bool(int(path[-2]))
|
||||
index = int(path[-1])
|
||||
|
|
@ -574,13 +650,13 @@ class Wallet():
|
|||
index = self.change_index if change else self.address_index
|
||||
desc = self.change_descriptor if change else self.recv_descriptor
|
||||
desc = desc.split("#")[0]
|
||||
return AddChecksum(desc.replace("*",f"{index}"))
|
||||
return AddChecksum(desc.replace("*", f"{index}"))
|
||||
|
||||
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):
|
||||
|
|
@ -590,11 +666,11 @@ class Wallet():
|
|||
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:
|
||||
|
|
@ -604,7 +680,7 @@ class Wallet():
|
|||
# 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]
|
||||
|
|
@ -622,10 +698,7 @@ class Wallet():
|
|||
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 = {}
|
||||
|
|
@ -637,16 +710,16 @@ class Wallet():
|
|||
self.change_keypool = end
|
||||
else:
|
||||
self.keypool = end
|
||||
self.rpc.multi([
|
||||
(
|
||||
"setlabel",
|
||||
self.get_address(i, change=change, check_keypool=False),
|
||||
"{} #{}".format(
|
||||
"Change" if change else "Address", i
|
||||
self.rpc.multi(
|
||||
[
|
||||
(
|
||||
"setlabel",
|
||||
self.get_address(i, change=change, check_keypool=False),
|
||||
"{} #{}".format("Change" if change else "Address", i),
|
||||
)
|
||||
)
|
||||
for i in range(start, end)
|
||||
])
|
||||
for i in range(start, end)
|
||||
]
|
||||
)
|
||||
self.save_to_file()
|
||||
return end
|
||||
|
||||
|
|
@ -655,19 +728,33 @@ class Wallet():
|
|||
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):
|
||||
return len([tx for tx in self.utxo if self.utxo_labels_list[tx["address"]] == label])
|
||||
return len(
|
||||
[tx for tx in self.utxo if self.utxo_labels_list[tx["address"]] == label]
|
||||
)
|
||||
|
||||
def balance_on_label(self, label):
|
||||
return sum(utxo["amount"] for utxo in self.utxo if self.utxo_labels_list[utxo["address"]] == label)
|
||||
return sum(
|
||||
utxo["amount"]
|
||||
for utxo in self.utxo
|
||||
if self.utxo_labels_list[utxo["address"]] == label
|
||||
)
|
||||
|
||||
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):
|
||||
|
|
@ -676,22 +763,28 @@ class Wallet():
|
|||
def utxo_addresses(self, idx=0, wallet_utxo_batch=100):
|
||||
return list(
|
||||
dict.fromkeys(
|
||||
list(reversed([
|
||||
utxo["address"] for utxo in sorted(
|
||||
self.utxo, key = lambda utxo: utxo["time"]
|
||||
list(
|
||||
reversed(
|
||||
[
|
||||
utxo["address"]
|
||||
for utxo in sorted(self.utxo, key=lambda utxo: utxo["time"])
|
||||
]
|
||||
)
|
||||
]))[(wallet_utxo_batch * idx):(wallet_utxo_batch * (idx + 1))]
|
||||
)[(wallet_utxo_batch * idx) : (wallet_utxo_batch * (idx + 1))]
|
||||
)
|
||||
)
|
||||
|
||||
def utxo_labels(self, idx=0, wallet_utxo_batch=100):
|
||||
return list(
|
||||
dict.fromkeys(
|
||||
list(reversed([
|
||||
self.utxo_labels_list[utxo["address"]] for utxo in sorted(
|
||||
self.utxo, key = lambda utxo: utxo["time"]
|
||||
list(
|
||||
reversed(
|
||||
[
|
||||
self.utxo_labels_list[utxo["address"]]
|
||||
for utxo in sorted(self.utxo, key=lambda utxo: utxo["time"])
|
||||
]
|
||||
)
|
||||
]))[(wallet_utxo_batch * idx):(wallet_utxo_batch * (idx + 1))]
|
||||
)[(wallet_utxo_batch * idx) : (wallet_utxo_batch * (idx + 1))]
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -703,18 +796,31 @@ class Wallet():
|
|||
|
||||
def getlabels(self, addresses):
|
||||
labels = {}
|
||||
addresses_infos = self.rpc.multi([('getaddressinfo', address) for address in addresses])
|
||||
addresses_infos = self.rpc.multi(
|
||||
[("getaddressinfo", address) for address in addresses]
|
||||
)
|
||||
for address_info in addresses_infos:
|
||||
address_info = address_info['result']
|
||||
address_info = address_info["result"]
|
||||
# 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_info["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_info["address"]
|
||||
)
|
||||
if label == "":
|
||||
label = address_info["address"]
|
||||
labels[address_info["address"]] = address_info["label"] if "label" in address_info and address_info["label"] != "" else label
|
||||
labels[address_info["address"]] = (
|
||||
address_info["label"]
|
||||
if "label" in address_info and address_info["label"] != ""
|
||||
else label
|
||||
)
|
||||
return labels
|
||||
|
||||
def get_address_name(self, address, addr_idx):
|
||||
|
|
@ -753,21 +859,35 @@ class Wallet():
|
|||
|
||||
@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):
|
||||
return self.addresses + self.change_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. If set to 0 Bitcoin Core sets feeRate automatically.
|
||||
fee_rate: in sat/B or BTC/kB. If set to 0 Bitcoin Core sets feeRate automatically.
|
||||
"""
|
||||
if fee_rate > 0 and fee_rate < self.MIN_FEE_RATE:
|
||||
fee_rate = self.MIN_FEE_RATE
|
||||
|
||||
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):
|
||||
|
|
@ -789,7 +909,9 @@ class Wallet():
|
|||
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
|
||||
|
|
@ -797,9 +919,9 @@ class Wallet():
|
|||
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))
|
||||
|
|
@ -810,41 +932,45 @@ class Wallet():
|
|||
|
||||
# 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)
|
||||
psbt_fees_sats = int(psbt["fee"] * 1e8)
|
||||
# estimate final size: add weight of inputs
|
||||
tx_full_size = ceil(psbt['tx']['vsize']
|
||||
+ len(psbt["inputs"])*self.weight_per_input/4)
|
||||
adjusted_fee_rate = fee_rate * (
|
||||
fee_rate / (psbt_fees_sats / psbt['tx']['vsize'])
|
||||
) * (tx_full_size / psbt['tx']['vsize'])
|
||||
options["feeRate"] = '%.8f' % round((adjusted_fee_rate * 1000) / 1e8, 8)
|
||||
tx_full_size = ceil(
|
||||
psbt["tx"]["vsize"] + len(psbt["inputs"]) * self.weight_per_input / 4
|
||||
)
|
||||
adjusted_fee_rate = (
|
||||
fee_rate
|
||||
* (fee_rate / (psbt_fees_sats / psbt["tx"]["vsize"]))
|
||||
* (tx_full_size / psbt["tx"]["vsize"])
|
||||
)
|
||||
options["feeRate"] = "%.8f" % round((adjusted_fee_rate * 1000) / 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"]
|
||||
psbt = self.rpc.decodepsbt(b64psbt)
|
||||
psbt["fee_rate"] = options["feeRate"]
|
||||
# estimate full size
|
||||
tx_full_size = ceil(psbt['tx']['vsize']
|
||||
+ len(psbt["inputs"])*self.weight_per_input/4)
|
||||
tx_full_size = ceil(
|
||||
psbt["tx"]["vsize"] + len(psbt["inputs"]) * self.weight_per_input / 4
|
||||
)
|
||||
psbt["tx_full_size"] = tx_full_size
|
||||
|
||||
psbt['base64'] = b64psbt
|
||||
psbt["base64"] = b64psbt
|
||||
psbt["amount"] = amounts
|
||||
psbt["address"] = addresses
|
||||
psbt["time"] = time.time()
|
||||
|
|
@ -854,12 +980,12 @@ class Wallet():
|
|||
|
||||
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:
|
||||
|
|
@ -878,15 +1004,15 @@ class Wallet():
|
|||
# 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()
|
||||
|
|
@ -916,12 +1042,15 @@ class Wallet():
|
|||
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]
|
||||
|
|
@ -948,18 +1077,18 @@ class Wallet():
|
|||
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
|
||||
# pubkeys
|
||||
input_size += 34 * len(self.keys)
|
||||
# signatures
|
||||
input_size += 75 * self.sigs_required
|
||||
|
||||
if not self.recv_descriptor.startswith('wsh'):
|
||||
if not self.recv_descriptor.startswith("wsh"):
|
||||
# P2SH scriptsig: 22 00 20 <32-byte-hash>
|
||||
input_size += 35 * 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: 16 00 14 20-byte-hash
|
||||
|
|
|
|||
|
|
@ -10,15 +10,17 @@ from .wallet import Wallet
|
|||
|
||||
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 @@ addrtypes = {
|
|||
"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 @@ class WalletManager:
|
|||
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,63 +83,59 @@ class WalletManager:
|
|||
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
|
||||
try:
|
||||
wallets[wallet_name] = Wallet.from_json(
|
||||
wallets_files[wallet],
|
||||
self.device_manager,
|
||||
self
|
||||
wallets_files[wallet], self.device_manager, self
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warn(f"Failed to load wallet {wallet_name}: {e}")
|
||||
logger.warn(
|
||||
f"Failed to load wallet {wallet_name}: {e}"
|
||||
)
|
||||
else:
|
||||
# wallet is loaded and should stay
|
||||
keep_wallets.append(wallet_name)
|
||||
|
|
@ -153,7 +143,9 @@ Silently ignored! RPC error: {e}")
|
|||
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}")
|
||||
|
|
@ -187,21 +179,24 @@ Silently ignored!" % wallet_alias)
|
|||
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]
|
||||
|
|
@ -216,14 +211,13 @@ Silently ignored!" % wallet_alias)
|
|||
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,
|
||||
|
|
@ -235,7 +229,7 @@ Silently ignored!" % wallet_alias)
|
|||
{},
|
||||
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:
|
||||
|
|
@ -244,13 +238,15 @@ Silently ignored!" % wallet_alias)
|
|||
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),
|
||||
|
|
@ -277,15 +273,14 @@ Silently ignored!" % wallet_alias)
|
|||
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:
|
||||
|
|
|
|||
|
|
@ -9,61 +9,83 @@ import time
|
|||
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@ from cryptoadvance.specter.helpers import which
|
|||
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()
|
||||
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,48 +1,51 @@
|
|||
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 == 302 # REDIRECT.
|
||||
result = client.get('/about')
|
||||
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 == 302 # REDIRECT.
|
||||
result = client.get("/about")
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -6,64 +6,66 @@ from cryptoadvance.specter.wallet_manager import WalletManager
|
|||
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
assert (
|
||||
helpers.which("date") == "/bin/date" or helpers.which("date") == "/usr/bin/date"
|
||||
) # travis-CI has it on /bin/date
|
||||
|
|
|
|||
|
|
@ -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.<br>You must first whitelist this website URL in HWIBridge settings to grant it access." },
|
||||
"id": None
|
||||
"error": {
|
||||
"code": -32001,
|
||||
"message": "Unauthorized request origin.<br>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)
|
||||
|
|
|
|||
|
|
@ -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 @@ class MerkleTreeTest(TestCase):
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,15 @@ import pytest
|
|||
|
||||
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"
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -7,17 +7,19 @@ from cryptoadvance.specter.wallet_manager import WalletManager
|
|||
|
||||
|
||||
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,38 +266,49 @@ 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.utxo_labels()) == sorted(['Random label', new_address])
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue