blacklify everything (#464)

This commit is contained in:
Stepan Snigirev 2020-09-30 11:42:30 +02:00 committed by GitHub
parent 318889bdca
commit 5702559317
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 3493 additions and 2425 deletions

View file

@ -1,4 +1,5 @@
from hwilib.devices import __all__
hiddenimports = []
for d in __all__:
hiddenimports.append('hwilib.devices.' + d)
hiddenimports.append("hwilib.devices." + d)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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",
)

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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):

View file

@ -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()

View file

@ -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

View file

@ -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())

View file

@ -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)

View 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

View file

@ -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

View file

@ -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"

View file

@ -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()

View file

@ -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

View file

@ -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):

View file

@ -1,4 +1,5 @@
from .hwi_device import HWIDevice
# a hack that verifies multisig
from .hwi import keepkey

View file

@ -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

View file

@ -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]

View file

@ -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"

View file

@ -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

View file

@ -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()

View file

@ -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,
)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View 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

View file

@ -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):

View file

@ -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

View file

@ -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]

View file

@ -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)

View file

@ -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
)

View file

@ -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

View file

@ -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

View file

@ -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...

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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:

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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"

View file

@ -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"

View file

@ -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