Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved export and restore hot wallet process #1495

Merged
merged 11 commits into from
Nov 30, 2021
3 changes: 2 additions & 1 deletion src/cryptoadvance/specter/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from .jade import Jade
from .generic import GenericDevice
from .electrum import Electrum
from .bitcoin_core import BitcoinCore
from .bitcoin_core import BitcoinCore, BitcoinCoreWatchOnly
from .elements_core import ElementsCore
from .seedsigner import SeedSignerDevice

Expand All @@ -31,6 +31,7 @@
SeedSignerDevice,
Electrum,
BitcoinCore,
BitcoinCoreWatchOnly,
ElementsCore,
GenericDevice,
]
67 changes: 58 additions & 9 deletions src/cryptoadvance/specter/devices/bitcoin_core.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import hmac
import logging
import os
import shutil
from embit import bip39, bip32, networks
from . import DeviceTypes
from io import BytesIO

from embit import bip32, bip39, networks

from ..device import Device
from ..helpers import alias
from ..util.descriptor import AddChecksum
from ..util.base58 import encode_base58_checksum, decode_base58
from ..util.xpub import get_xpub_fingerprint, convert_xpub_prefix
from ..key import Key
from ..rpc import get_default_datadir
from io import BytesIO
import hmac
import logging
from ..specter_error import SpecterError
from ..util.base58 import decode_base58, encode_base58_checksum
from ..util.descriptor import AddChecksum
from ..util.xpub import convert_xpub_prefix, get_xpub_fingerprint
from . import DeviceTypes

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -74,7 +77,7 @@ def taproot_available(self, rpc):
return taproot_support
except Exception as e:
self.taproot_support = False
logger.error(e)
logger.exception(e)
return False

def add_hot_wallet_keys(
Expand Down Expand Up @@ -259,3 +262,49 @@ def delete(
break
except:
pass # We tried...


class BitcoinCoreWatchOnly(BitcoinCore):
"""If a BitcoinCore Hotwallet get exported, it'll have the type:"bitcoincore_watchonly" .
if such a device.json get imported, it's instantiate as a BitcoinCoreWatchOnly.
It can be converted back to a device of Type BitcoinCore by providing the 12 words again.
"""

device_type = DeviceTypes.BITCOINCORE_WATCHONLY
name = "Bitcoin Core (watch only)"
hot_wallet = False

def sign_psbt(self, base64_psbt, wallet, file_password=None):
raise SpecterError("Cannot sign with a watch-only wallet. Convert")

def sign_raw_tx(self, raw_tx, wallet, file_password=None):
raise SpecterError("Cannot sign with a watch-only wallet")

def add_hot_wallet_keys(
self,
mnemonic,
passphrase,
paths,
file_password,
wallet_manager,
testnet,
keys_range=[0, 1000],
keys_purposes=[],
):
# Convert the watch-only wallet to a hot wallet then fix up its internal attrs to
# match.
super().add_hot_wallet_keys(
mnemonic,
passphrase,
paths,
file_password,
wallet_manager,
testnet,
keys_range,
keys_purposes,
)

# Change type (also triggers write to file)
self.set_type(DeviceTypes.BITCOINCORE)
# After update this device will be available as a BitcoinCore (hot) instance
self.manager.update()
1 change: 1 addition & 0 deletions src/cryptoadvance/specter/devices/device_types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class DeviceTypes:
BITBOX02 = "bitbox02"
BITCOINCORE = "bitcoincore"
BITCOINCORE_WATCHONLY = "bitcoincore_watchonly"
COBO = "cobo"
COLDCARD = "coldcard"
ELECTRUM = "electrum"
Expand Down
14 changes: 11 additions & 3 deletions src/cryptoadvance/specter/managers/device_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import pathlib
from flask_babel import lazy_gettext as _

from cryptoadvance.specter.devices.bitcoin_core import BitcoinCoreWatchOnly

from ..helpers import alias, load_jsons
from ..rpc import get_default_datadir

Expand Down Expand Up @@ -101,25 +103,31 @@ def supported_devices(self):
return device_classes

def supported_devices_for_chain(self, specter):
"""Devices which you can create via the UI. BitcoinCoreWatchonly is not among them
and if we have no node, hot-wallets neither. Liquid support is limited as well.
"""
if not specter.chain:
return [
devices = [
device_class
for device_class in device_classes
if device_class.device_type != "bitcoincore"
and device_class.device_type != "elementscore"
]
elif specter.is_liquid:
return [
devices = [
device_class
for device_class in device_classes
if device_class.liquid_support
]
else:
return [
devices = [
device_class
for device_class in device_classes
if device_class.bitcoin_core_support
]
if BitcoinCoreWatchOnly in devices:
devices.remove(BitcoinCoreWatchOnly)
return devices

def delete(self, specter):
"""Deletes all the devices"""
Expand Down
4 changes: 2 additions & 2 deletions src/cryptoadvance/specter/managers/wallet_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def __init__(
self.WalletClass = LWallet if is_liquid(chain) else Wallet
self.update(data_folder, rpc, chain)

def update(self, data_folder=None, rpc=None, chain=None, allow_threading=True):
def update(self, data_folder=None, rpc=None, chain=None, use_threading=True):
if self.is_loading:
return
self.is_loading = True
Expand Down Expand Up @@ -102,7 +102,7 @@ def update(self, data_folder=None, rpc=None, chain=None, allow_threading=True):
for k in list(self.wallets.keys()):
if k not in self.wallets_update_list:
self.wallets.pop(k)
if allow_threading and self.allow_threading:
if self.allow_threading and use_threading:
t = threading.Thread(
target=self._update,
args=(
Expand Down
12 changes: 11 additions & 1 deletion src/cryptoadvance/specter/server_endpoints/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from flask_babel import lazy_gettext as _
from flask_login import login_required, current_user
from mnemonic import Mnemonic

from cryptoadvance.specter.devices.device_types import DeviceTypes
from ..devices.bitcoin_core import BitcoinCore
from ..helpers import is_testnet, generate_mnemonic, validate_mnemonic
from ..key import Key
Expand Down Expand Up @@ -84,11 +86,19 @@ def new_device_keys(device_type):
err = _("Failed to parse these xpubs") + ":\n" + "\n".join(xpub)
break
if not keys and not err:
if device_type in ["bitcoincore", "elementscore"]:
if device_type in [
DeviceTypes.BITCOINCORE,
DeviceTypes.ELEMENTSCORE,
DeviceTypes.BITCOINCORE_WATCHONLY,
]:
if not paths:
err = _("No paths were specified, please provide at least one.")
if err is None:
if existing_device:
if device_type == DeviceTypes.BITCOINCORE_WATCHONLY:
device.setup_device(
file_password, app.specter.wallet_manager
)
device.add_hot_wallet_keys(
mnemonic,
passphrase,
Expand Down
3 changes: 2 additions & 1 deletion src/cryptoadvance/specter/server_endpoints/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ def general():
f"Wallet {wallet['alias']} already exists, skipping creation"
)
write_wallet(wallet)
app.specter.wallet_manager.update(allow_threading=False)
app.specter.wallet_manager.update(use_threading=False)

try:
wallet_obj = app.specter.wallet_manager.get_by_alias(
wallet["alias"]
Expand Down
8 changes: 7 additions & 1 deletion src/cryptoadvance/specter/specter.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from stem.control import Controller
from urllib3.exceptions import NewConnectionError

from cryptoadvance.specter.devices.device_types import DeviceTypes

from .helpers import clean_psbt, deep_update, is_liquid, is_testnet, get_asset_label
from .internal_node import InternalNode
from .liquid.rpc import LiquidRPC
Expand Down Expand Up @@ -665,8 +667,12 @@ def specter_backup_file(self):
data = zipfile.ZipInfo("{}.json".format(device.alias))
data.date_time = time.localtime(time.time())[:6]
data.compress_type = zipfile.ZIP_DEFLATED
device = device.json
# Exporting the bitcoincore hot wallet as watchonly
if device["type"] == DeviceTypes.BITCOINCORE:
device["type"] = DeviceTypes.BITCOINCORE_WATCHONLY
zf.writestr(
"devices/{}.json".format(device.alias), json.dumps(device.json)
"devices/{}.json".format(device["alias"]), json.dumps(device)
)
memory_file.seek(0)
return memory_file
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
<select name="device_type" id="device_type">
<option value="" selected>{{ _("Select type") }}</option>
{% for cls in specter.device_manager.supported_devices %}
<option value="{{cls.device_type}}">{{cls.name}}</option>
{% if cls.device_type != "bitcoincore" or (device and device.device_type != "bitcoincore_watchonly") %}
<option value="{{cls.device_type}}">{{cls.name}}</option>
{% endif %}
{% endfor %}
</select>
</fieldset>
Expand Down
6 changes: 5 additions & 1 deletion src/cryptoadvance/specter/templates/device/device.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@
<div style="line-height: 1; margin-top: 30px;">
<form action="./" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<button id="add_keys" type="submit" name="action" value="add_keys" class="btn centered">{{ _("Add more keys") }}</button>
{% if device.device_type != "bitcoincore_watchonly"%}
<button id="add_keys" type="submit" name="action" value="add_keys" class="btn centered">{{ _("Add more keys") }}</button>
{% else %}
<button id="add_keys" type="submit" name="action" value="add_keys" class="btn centered">{{ _("Convert to hot wallet") }}</button>
{% endif %}
</form>
<br>
{% if device.hwi_support %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
{{ _("To pair your Passport with Specter, navigate to Pair Wallet –> Specter and follow the instructions on your Passport.<br>If your webcam is having difficulty scanning QR codes on Passport’s screen, we recommend using a microSD.") }}
</span>
</div>
<div id="hwi-only-instructions" {%if device_class.device_type in ['bitcoincore', 'cobo', 'coldcard', 'electrum', 'other', 'specter']%}class="hidden"{% endif %}>
<div id="hwi-only-instructions" {%if device_class.device_type in ['bitcoincore', 'bitcoincore_watchonly', 'cobo', 'coldcard', 'electrum', 'other', 'specter']%}class="hidden"{% endif %}>
<p>{{ _("Connect your hardware device to the computer via USB.") }}</p>
</div>
<br>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
<div id="device_setup_wizard" class="card center" style="width: auto; min-width: 90%; margin: 40px;">
<div id="device-type-container">
<h1 style="font-size: 1.8em;">{{ _("Select Your Device Type") }}</h1>
<input id="device-type-searchbar" type="text" placeholder='{{ _("Filter devices...") }}' style="width: 60%; font-size: 1.4em; padding: 10px;" oninput="filterDeviceTypes(this.value)" {% if (specter.device_manager.supported_devices_for_chain(specter)|length) < 8 %}class="hidden"{% endif %}>
<input id="device-type-searchbar" type="text" placeholder='{{ _("Filter devices...") }}' style="width: 60%; font-size: 1.4em; padding: 10px;" oninput="filterDeviceTypes(this.value)">
<div id="devices_list" class="row overflow">
{% for cls in specter.device_manager.supported_devices_for_chain(specter) %}
{% if not cls.hot_wallet or specter.rpc %}
{% if not cls.hot_wallet or specter.rpc %}
<a id="label_device_type_{{ cls.name | replace(' ', '') }}" href="{{ url_for('devices_endpoint.new_device_keys', device_type=cls.device_type) if not cls.hot_wallet else url_for('devices_endpoint.new_device_mnemonic', device_type=cls.device_type) }}" style="text-decoration: none;">
<div class="small-card radio" id="{{ cls.device_type }}_device_card" style="transform: scale(0.85); margin: 3px;">
<img src="{{ url_for('static', filename='img/devices/' ~ cls.icon) }}" width="18px">
Expand All @@ -18,7 +18,7 @@
{% endfor %}
</div>
<a class="note center" style="text-decoration: underline; cursor: pointer;" href="{{ url_for('devices_endpoint.new_device_manual') }}">{{ _("Manual configuration") }}</a>
</div>
</div>supported_devices_for_chain
</div>
{% endblock %}

Expand All @@ -28,14 +28,22 @@
let devicesLabels = []
{% for cls in specter.device_manager.supported_devices_for_chain(specter) %}
{% if not cls.hot_wallet or specter.rpc %}

devicesLabels.push('label_device_type_{{ cls.name | replace(' ', '') }}')
{% endif %}
{% endfor %}
if (text) {
for (let deviceLabel of devicesLabels) {
if (deviceLabel.split('label_device_type_')[1].toLowerCase().includes(text.toLowerCase())) {
console.log(deviceLabel)
console.log(document.getElementById(deviceLabel))
console.log(document.getElementById(deviceLabel).style.display)
document.getElementById(deviceLabel).style.display = 'flex';
} else {
console.log("non")
console.log(deviceLabel)
console.log(document.getElementById(deviceLabel))
console.log(document.getElementById(deviceLabel).style.display)
document.getElementById(deviceLabel).style.display = 'none';
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ m/48h/{{ 0 if specter.info.chain == "main" else 1 }}h/0h/2h</textarea>
function setPubkeysView() {
var coldDevice = document.getElementById("cold_device");
var hotDevice = document.getElementById("hot_device");
if (deviceType.value == 'bitcoincore') {
if (deviceType.value == 'bitcoincore' || deviceType.value == 'bitcoincore_watchonly') {
coldDevice.style.display = 'none';
hotDevice.style.display = 'block';
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,15 @@
<div id="signing_container" class="signing_container flex-column {% if psbt['raw'] %}hidden{% endif %}" style="text-align: center;">
<p style="margin-bottom: 15px; margin-top: 0px;">{{ _("Sign transaction with your:") }}</p>
{% for device in wallet.devices %}
{% set device_signed = (device.alias in psbt.get("devices_signed")) %}
<button type="button" class="btn signing-column-btn" id="{{ device.alias }}_tx_sign_btn" {% if device_signed %} disabled style="background-color:#303c49;" {% endif %}>
{{ device.name }} {% if device.alias in psbt.get('devices_signed',[]) %} (&#10004;) {% endif %}
</button>
{% if device.type != "bitcoincore_watchonly" %}
{% set device_signed = (device.alias in psbt.get("devices_signed")) %}
<button type="button" class="btn signing-column-btn" id="{{ device.alias }}_tx_sign_btn" {% if device_signed %} disabled style="background-color:#303c49;" {% endif %}>
{{ device.name }} {% if device.alias in psbt.get('devices_signed',[]) %} (&#10004;) {% endif %}
</button>
{% endif %}
{% if device.type == "bitcoincore_watchonly" %}
<p>{{ _("Cannot sign with a watchonly wallet") }}</p>
{% endif %}
{% endfor %}
</div>

Expand Down
2 changes: 1 addition & 1 deletion utils/test-cypress.sh
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ function start_specter {
specter_pid=$!
# Simulate slower machines with uncommenting this (-l 10 means using 10% cpu):
#cpulimit -p $specter_pid -l 10 -b
$(npm bin)/wait-on http://localhost:${PORT}
$(npm bin)/wait-on http://127.0.0.1:${PORT}
}

function stop_specter {
Expand Down