mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
Chore: Refactoring new device setup (#1111)
* New device refactoring * Hide hot wallet if RPC is not connected * Replace old url with new one * Update new_device_keys.jinja * Fixes for adding keys to existing device * Cleanup old code for adding keys * Fix cypress * Update cypress * Fix hot wallet issue * Hide generate option on add keys hot wallet Co-authored-by: Kim Neunert <k9ert@gmx.de>
This commit is contained in:
parent
9039e33f11
commit
7dcfbdc3d9
14 changed files with 488 additions and 578 deletions
|
|
@ -35,13 +35,13 @@ Cypress.Commands.add("addDevice", (name) => {
|
|||
// Creating a Device
|
||||
cy.contains('Select Your Device Type')
|
||||
cy.get('#trezor_device_card')
|
||||
cy.get('#step1 > [type="text"]').type("specter")
|
||||
cy.get('#device-type-searchbar').type("specter")
|
||||
cy.contains('Select Your Device Type')
|
||||
cy.get('#trezor_device_card').should('not.have.class', 'disabled')
|
||||
cy.get('#specter_device_card').click()
|
||||
cy.get('h2 > input').type(name)
|
||||
cy.get('#wizard-previous').click()
|
||||
cy.get('#step1 > .note').click()
|
||||
cy.go('back')
|
||||
cy.get('#device-type-container > .note').click()
|
||||
cy.get('#device_name').type(name)
|
||||
cy.get('#device_type').select("Specter-DIY")
|
||||
cy.get('#txt').type("[6ea15da6/84h/1h/0h]vpub5Yw9Qps1aUVBpD3eyKVUhe5K8gbaWav9ArB4FudKeCLPb5vSsX8afAWfYvEASbkv4qxKzxSRyd3wqdgM3ir2fbrUJwiHDAJUNWGeqMDQfTj")
|
||||
|
|
@ -64,9 +64,9 @@ Cypress.Commands.add("addHotDevice", (name) => {
|
|||
// Creating a Device
|
||||
cy.contains('Select Your Device Type')
|
||||
cy.get('#bitcoincore_device_card').click()
|
||||
cy.get('#wizard-next').click()
|
||||
cy.get('#submit-mnemonic').click()
|
||||
cy.get('#device_name').type(name)
|
||||
cy.get('#wizard-submit').click()
|
||||
cy.get('#submit-keys').click()
|
||||
cy.get('#devices_list > .item > div').contains(name)
|
||||
})
|
||||
})
|
||||
32
package-lock.json
generated
32
package-lock.json
generated
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "specter-desktop-cypress-testing",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 2,
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
|
|
@ -3483,21 +3483,6 @@
|
|||
"tweetnacl": "~0.14.0"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
|
||||
|
|
@ -3523,6 +3508,21 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ class BitcoinCore(Device):
|
|||
keys_range=[0, 1000],
|
||||
keys_purposes=[],
|
||||
):
|
||||
if type(keys_range[0]) == str:
|
||||
keys_range[0] = int(keys_range[0])
|
||||
if type(keys_range[1]) == str:
|
||||
keys_range[1] = int(keys_range[1])
|
||||
seed = bip39.mnemonic_to_seed(mnemonic, passphrase)
|
||||
root = bip32.HDKey.from_seed(seed)
|
||||
network = networks.NETWORKS["test" if testnet else "main"]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import copy, random
|
||||
import copy, random, json
|
||||
|
||||
from flask import (
|
||||
Flask,
|
||||
|
|
@ -15,6 +15,8 @@ from flask import current_app as app
|
|||
from mnemonic import Mnemonic
|
||||
from ..helpers import is_testnet, generate_mnemonic
|
||||
from ..key import Key
|
||||
from ..device_manager import get_device_class
|
||||
from ..devices.bitcoin_core import BitcoinCore
|
||||
from ..wallet_manager import purposes
|
||||
from ..specter_error import handle_exception
|
||||
|
||||
|
|
@ -25,138 +27,195 @@ devices_endpoint = Blueprint("devices_endpoint", __name__)
|
|||
|
||||
|
||||
################## New device #######################
|
||||
|
||||
# New device
|
||||
@devices_endpoint.route("/new_device/", methods=["GET", "POST"])
|
||||
# New device type
|
||||
@devices_endpoint.route("/new_device_type/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def new_device():
|
||||
def new_device_type():
|
||||
return render_template(
|
||||
"device/new_device/new_device_type.jinja",
|
||||
specter=app.specter,
|
||||
rand=rand,
|
||||
)
|
||||
|
||||
|
||||
@devices_endpoint.route("/new_device_keys/<device_type>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def new_device_keys(device_type):
|
||||
err = None
|
||||
strength = 128
|
||||
mnemonic = generate_mnemonic(strength=strength)
|
||||
mnemonic = ""
|
||||
passphrase = ""
|
||||
file_password = ""
|
||||
range_start = 0
|
||||
range_end = 1000
|
||||
existing_device = None
|
||||
if request.method == "POST":
|
||||
if request.form.get("existing_device"):
|
||||
device = app.specter.device_manager.get_by_alias(
|
||||
request.form.get("existing_device")
|
||||
)
|
||||
device_type = device.device_type
|
||||
mnemonic = request.form.get("mnemonic", "")
|
||||
passphrase = request.form.get("passphrase", "")
|
||||
file_password = request.form.get("file_password", "")
|
||||
range_start = int(request.form.get("range_start", "0"))
|
||||
range_end = int(request.form.get("range_end", "1000"))
|
||||
existing_device = request.form.get("existing_device", None)
|
||||
if existing_device:
|
||||
device = app.specter.device_manager.get_by_alias(existing_device)
|
||||
else:
|
||||
device_type = request.form.get("devices")
|
||||
device_name = request.form.get("device_name", "")
|
||||
if not device_name:
|
||||
err = "Device name must not be empty"
|
||||
elif device_name in app.specter.device_manager.devices_names:
|
||||
err = "Device with this name already exists"
|
||||
xpubs_rows_count = int(request.form["xpubs_rows_count"]) + 1
|
||||
if device_type != "bitcoincore":
|
||||
keys = []
|
||||
for i in range(0, xpubs_rows_count):
|
||||
purpose = request.form.get(
|
||||
"xpubs-table-row-{}-purpose".format(i), "Custom"
|
||||
)
|
||||
xpub = request.form.get("xpubs-table-row-{}-xpub-hidden".format(i), "-")
|
||||
if xpub != "-":
|
||||
try:
|
||||
keys.append(Key.parse_xpub(xpub, purpose=purpose))
|
||||
except:
|
||||
err = "Failed to parse these xpubs:\n" + "\n".join(xpub)
|
||||
break
|
||||
if not keys and not err:
|
||||
err = "xpubs name must not be empty"
|
||||
if err is None:
|
||||
if request.form.get("existing_device"):
|
||||
device.add_keys(keys)
|
||||
flash("{} keys were added successfully".format(len(keys)))
|
||||
return redirect(
|
||||
url_for("devices_endpoint.device", device_alias=device.alias)
|
||||
keys = []
|
||||
paths = []
|
||||
keys_purposes = []
|
||||
for i in range(0, xpubs_rows_count):
|
||||
purpose = request.form.get("xpubs-table-row-{}-purpose".format(i), "Custom")
|
||||
xpub = request.form.get("xpubs-table-row-{}-xpub-hidden".format(i), "-")
|
||||
path = request.form.get(
|
||||
"xpubs-table-row-{}-derivation-hidden".format(i), ""
|
||||
)
|
||||
if path != "":
|
||||
paths.append(path)
|
||||
keys_purposes.append(purpose)
|
||||
if xpub != "-":
|
||||
try:
|
||||
keys.append(Key.parse_xpub(xpub, purpose=purpose))
|
||||
except:
|
||||
err = "Failed to parse these xpubs:\n" + "\n".join(xpub)
|
||||
break
|
||||
if not keys and not err:
|
||||
if device_type == "bitcoincore":
|
||||
if not paths:
|
||||
err = "No paths were specified, please provide at lease one."
|
||||
if err is None:
|
||||
if existing_device:
|
||||
device.add_hot_wallet_keys(
|
||||
mnemonic,
|
||||
passphrase,
|
||||
paths,
|
||||
file_password,
|
||||
app.specter.wallet_manager,
|
||||
is_testnet(app.specter.chain),
|
||||
keys_range=[range_start, range_end],
|
||||
keys_purposes=keys_purposes,
|
||||
)
|
||||
flash("{} keys were added successfully".format(len(paths)))
|
||||
return redirect(
|
||||
url_for(
|
||||
"devices_endpoint.device", device_alias=device.alias
|
||||
)
|
||||
)
|
||||
device = app.specter.device_manager.add_device(
|
||||
name=device_name, device_type=device_type, keys=[]
|
||||
)
|
||||
device = app.specter.device_manager.add_device(
|
||||
name=device_name, device_type=device_type, keys=keys
|
||||
)
|
||||
flash("{} was added successfully!".format(device_name))
|
||||
try:
|
||||
device.setup_device(file_password, app.specter.wallet_manager)
|
||||
device.add_hot_wallet_keys(
|
||||
mnemonic,
|
||||
passphrase,
|
||||
paths,
|
||||
file_password,
|
||||
app.specter.wallet_manager,
|
||||
is_testnet(app.specter.chain),
|
||||
keys_range=[range_start, range_end],
|
||||
keys_purposes=keys_purposes,
|
||||
)
|
||||
flash("{} was added successfully!".format(device_name))
|
||||
return redirect(
|
||||
url_for(
|
||||
"devices_endpoint.device", device_alias=device.alias
|
||||
)
|
||||
+ "?newdevice=true"
|
||||
)
|
||||
except Exception as e:
|
||||
handle_exception(e)
|
||||
flash(f"Failed to setup hot wallet. Error: {e}", "error")
|
||||
app.specter.device_manager.remove_device(
|
||||
device,
|
||||
app.specter.wallet_manager,
|
||||
bitcoin_datadir=app.specter.bitcoin_datadir,
|
||||
chain=app.specter.chain,
|
||||
)
|
||||
else:
|
||||
err = "xpubs list must not be empty"
|
||||
elif not err:
|
||||
if existing_device:
|
||||
device.add_keys(keys)
|
||||
flash("{} keys were added successfully".format(len(keys)))
|
||||
return redirect(
|
||||
url_for("devices_endpoint.device", device_alias=device.alias)
|
||||
+ "?newdevice=true"
|
||||
)
|
||||
else:
|
||||
flash(err, "error")
|
||||
else:
|
||||
if len(request.form["mnemonic"].split(" ")) not in [12, 15, 18, 21, 24]:
|
||||
err = "Invalid mnemonic entered: Must contain either: 12, 15, 18, 21, or 24 words."
|
||||
mnemo = Mnemonic("english")
|
||||
if not mnemo.check(request.form["mnemonic"]):
|
||||
err = "Invalid mnemonic entered."
|
||||
range_start = int(request.form["range_start"])
|
||||
range_end = int(request.form["range_end"])
|
||||
if range_start > range_end:
|
||||
err = "Invalid address range selected."
|
||||
mnemonic = request.form["mnemonic"]
|
||||
paths = []
|
||||
keys_purposes = []
|
||||
for i in range(0, xpubs_rows_count):
|
||||
purpose = request.form.get(
|
||||
"xpubs-table-row-{}-purpose".format(i), "Custom"
|
||||
)
|
||||
path = request.form.get(
|
||||
"xpubs-table-row-{}-derivation-hidden".format(i), ""
|
||||
)
|
||||
if path != "":
|
||||
paths.append(path)
|
||||
keys_purposes.append(purpose)
|
||||
if not paths:
|
||||
err = "No paths were specified, please provide at lease one."
|
||||
if err is None:
|
||||
passphrase = request.form["passphrase"]
|
||||
file_password = request.form["file_password"]
|
||||
if request.form.get("existing_device"):
|
||||
device.add_hot_wallet_keys(
|
||||
mnemonic,
|
||||
passphrase,
|
||||
paths,
|
||||
file_password,
|
||||
app.specter.wallet_manager,
|
||||
is_testnet(app.specter.chain),
|
||||
keys_range=[range_start, range_end],
|
||||
keys_purposes=keys_purposes,
|
||||
)
|
||||
flash("{} keys were added successfully".format(len(paths)))
|
||||
return redirect(
|
||||
url_for("devices_endpoint.device", device_alias=device.alias)
|
||||
)
|
||||
device = app.specter.device_manager.add_device(
|
||||
name=device_name, device_type=device_type, keys=[]
|
||||
)
|
||||
try:
|
||||
device.setup_device(file_password, app.specter.wallet_manager)
|
||||
device.add_hot_wallet_keys(
|
||||
mnemonic,
|
||||
passphrase,
|
||||
paths,
|
||||
file_password,
|
||||
app.specter.wallet_manager,
|
||||
is_testnet(app.specter.chain),
|
||||
keys_range=[range_start, range_end],
|
||||
keys_purposes=keys_purposes,
|
||||
)
|
||||
flash("{} was added successfully!".format(device_name))
|
||||
return redirect(
|
||||
url_for("devices_endpoint.device", device_alias=device.alias)
|
||||
+ "?newdevice=true"
|
||||
)
|
||||
except Exception as e:
|
||||
handle_exception(e)
|
||||
flash(f"Failed to setup hot wallet. Error: {e}", "error")
|
||||
app.specter.device_manager.remove_device(
|
||||
device,
|
||||
app.specter.wallet_manager,
|
||||
bitcoin_datadir=app.specter.bitcoin_datadir,
|
||||
chain=app.specter.chain,
|
||||
)
|
||||
else:
|
||||
flash(err, "error")
|
||||
device = app.specter.device_manager.add_device(
|
||||
name=device_name, device_type=device_type, keys=keys
|
||||
)
|
||||
flash("{} was added successfully!".format(device_name))
|
||||
return redirect(
|
||||
url_for("devices_endpoint.device", device_alias=device.alias)
|
||||
+ "?newdevice=true"
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"device/new_device.jinja",
|
||||
"device/new_device/new_device_keys.jinja",
|
||||
device_class=get_device_class(device_type),
|
||||
mnemonic=mnemonic,
|
||||
passphrase=passphrase,
|
||||
file_password=file_password,
|
||||
range_start=range_start,
|
||||
range_end=range_end,
|
||||
existing_device=app.specter.device_manager.get_by_alias(existing_device)
|
||||
if existing_device
|
||||
else None,
|
||||
error=err,
|
||||
specter=app.specter,
|
||||
rand=rand,
|
||||
)
|
||||
|
||||
|
||||
@devices_endpoint.route("/new_device_mnemonic/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def new_device_mnemonic():
|
||||
err = None
|
||||
strength = 128
|
||||
mnemonic = generate_mnemonic(strength=strength)
|
||||
existing_device = None
|
||||
if request.method == "POST":
|
||||
if len(request.form["mnemonic"].split(" ")) not in [12, 15, 18, 21, 24]:
|
||||
err = "Invalid mnemonic entered: Must contain either: 12, 15, 18, 21, or 24 words."
|
||||
mnemo = Mnemonic("english")
|
||||
if not mnemo.check(request.form["mnemonic"]):
|
||||
err = "Invalid mnemonic entered."
|
||||
range_start = int(request.form["range_start"])
|
||||
range_end = int(request.form["range_end"])
|
||||
if range_start > range_end:
|
||||
err = "Invalid address range selected."
|
||||
mnemonic = request.form["mnemonic"]
|
||||
passphrase = request.form["passphrase"]
|
||||
file_password = request.form["file_password"]
|
||||
existing_device = request.form.get("existing_device", None)
|
||||
print("file_password")
|
||||
print(file_password)
|
||||
if existing_device:
|
||||
existing_device = app.specter.device_manager.get_by_alias(existing_device)
|
||||
if not err:
|
||||
return render_template(
|
||||
"device/new_device/new_device_keys.jinja",
|
||||
mnemonic=mnemonic,
|
||||
passphrase=passphrase,
|
||||
file_password=file_password,
|
||||
range_start=range_start,
|
||||
range_end=range_end,
|
||||
device_class=BitcoinCore,
|
||||
existing_device=existing_device,
|
||||
error=err,
|
||||
specter=app.specter,
|
||||
rand=rand,
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"device/new_device/new_device_mnemonic.jinja",
|
||||
strength=strength,
|
||||
mnemonic=mnemonic,
|
||||
existing_device=existing_device,
|
||||
error=err,
|
||||
specter=app.specter,
|
||||
rand=rand,
|
||||
)
|
||||
|
|
@ -312,62 +371,26 @@ def device(device_alias):
|
|||
elif action == "add_keys":
|
||||
strength = 128
|
||||
mnemonic = generate_mnemonic(strength=strength)
|
||||
return render_template(
|
||||
"device/new_device.jinja",
|
||||
mnemonic=mnemonic,
|
||||
strength=strength,
|
||||
existing_device=device,
|
||||
device_alias=device_alias,
|
||||
specter=app.specter,
|
||||
rand=rand,
|
||||
)
|
||||
elif action == "morekeys":
|
||||
if device.hot_wallet:
|
||||
if len(request.form["mnemonic"].split(" ")) not in [12, 15, 18, 21, 24]:
|
||||
err = "Invalid mnemonic entered: Must contain either: 12, 15, 18, 21, or 24 words."
|
||||
mnemo = Mnemonic("english")
|
||||
if not mnemo.check(request.form["mnemonic"]):
|
||||
err = "Invalid mnemonic entered."
|
||||
range_start = int(request.form["range_start"])
|
||||
range_end = int(request.form["range_end"])
|
||||
if range_start > range_end:
|
||||
err = "Invalid address range selected."
|
||||
if err is None:
|
||||
mnemonic = request.form["mnemonic"]
|
||||
paths = [
|
||||
l.strip()
|
||||
for l in request.form["derivation_paths"].split("\n")
|
||||
if len(l) > 0
|
||||
]
|
||||
passphrase = request.form["passphrase"]
|
||||
file_password = request.form["file_password"]
|
||||
device.add_hot_wallet_keys(
|
||||
mnemonic,
|
||||
passphrase,
|
||||
paths,
|
||||
file_password,
|
||||
app.specter.wallet_manager,
|
||||
is_testnet(app.specter.chain),
|
||||
keys_range=[range_start, range_end],
|
||||
)
|
||||
return render_template(
|
||||
"device/new_device/new_device_mnemonic.jinja",
|
||||
mnemonic=mnemonic,
|
||||
strength=strength,
|
||||
existing_device=device,
|
||||
device_alias=device_alias,
|
||||
device_class=get_device_class(device.device_type),
|
||||
specter=app.specter,
|
||||
rand=rand,
|
||||
)
|
||||
else:
|
||||
# refactor to fn
|
||||
xpubs = request.form["xpubs"]
|
||||
keys, failed = Key.parse_xpubs(xpubs)
|
||||
err = None
|
||||
if len(failed) > 0:
|
||||
err = "Failed to parse these xpubs:\n" + "\n".join(failed)
|
||||
return render_template(
|
||||
"device/new_device_manual.jinja",
|
||||
device=device,
|
||||
device_alias=device_alias,
|
||||
xpubs=xpubs,
|
||||
error=err,
|
||||
specter=app.specter,
|
||||
rand=rand,
|
||||
)
|
||||
if err is None:
|
||||
device.add_keys(keys)
|
||||
return render_template(
|
||||
"device/new_device/new_device_keys.jinja",
|
||||
existing_device=device,
|
||||
device_alias=device_alias,
|
||||
device_class=get_device_class(device.device_type),
|
||||
specter=app.specter,
|
||||
rand=rand,
|
||||
)
|
||||
elif action == "settype":
|
||||
device_type = request.form["device_type"]
|
||||
device.set_type(device_type)
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@
|
|||
<img src="{{ url_for('static', filename='img/checkbox-' + ('un' if not (specter.device_manager.devices | length) else '') + 'tick.svg') }}" width="30px">
|
||||
</td>
|
||||
<td style="text-align: left;">
|
||||
<a href="{{ url_for('devices_endpoint.new_device') }}">
|
||||
<a href="{{ url_for('devices_endpoint.new_device_type') }}">
|
||||
<h2>Add signing devices you wish to use.</h2>
|
||||
</a>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@
|
|||
<div class="hidden" id="new_device_popup">
|
||||
<h1>New device was added successfully!</h1>
|
||||
<div class="row overflow">
|
||||
<a href="{{ url_for('devices_endpoint.new_device') }}" style="text-decoration: none;">
|
||||
<a href="{{ url_for('devices_endpoint.new_device_type') }}" style="text-decoration: none;">
|
||||
<div class="small-card radio">
|
||||
<img src="{{ url_for('static', filename='img/plus.svg') }}" class="svg-white" style="transform: scale(1.5);">
|
||||
Add another device
|
||||
|
|
|
|||
|
|
@ -32,78 +32,26 @@
|
|||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
<div id="device_setup_wizard" style="width: 100%;" onsubmit="showPacman();">
|
||||
<h1 style="font-size: 2em;">Add {% if existing_device %}Keys{% else %}Device{% endif %}</h1><br>
|
||||
<form id="form" action="{{ url_for('devices_endpoint.new_device') }}" method="POST" class="card center" style="width: auto; margin: 40px;">
|
||||
<form id="form" action="{{ url_for('devices_endpoint.new_device_keys', device_type=device_class.device_type) }}" method="POST" style="width: 100%;" onsubmit="showPacman();">
|
||||
<div style="width: auto; margin: 40px;" class="card center" >
|
||||
{% if existing_device %}
|
||||
<input type="hidden" name="existing_device" value="{{ existing_device.alias }}">
|
||||
{% endif %}
|
||||
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div {% if not existing_device %}id="step1"{% else %}class="hidden"{% endif %}>
|
||||
<h1 style="font-size: 1.8em;">Select Your Device Type</h1>
|
||||
<input 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 %}
|
||||
<label id="label_device_type_{{ cls.name }}">
|
||||
<input onchange="setStepCompleted(1);nextStep();" type="radio" name="devices" value="{{ cls.device_type }}" data-qrcode="{{cls.qr_code_support}}" data-sdcard="{{cls.sd_card_support}}" data-hwi="{{cls.hwi_support}}" data-device-name="{{ cls.name }}" class="hidden device-type">
|
||||
<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">
|
||||
{{ cls.name }}
|
||||
</div>
|
||||
</label>
|
||||
{% 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 {% if not existing_device %}id="step2-hot-wallet" class="hidden"{% elif existing_device.hot_wallet %}id="step1"{% else %}class="hidden"{% endif %}>
|
||||
<div id="hot-wallet" style="width: 80%; max-width: 700px; margin: auto;">
|
||||
<br>
|
||||
<p class="note" style="border-radius: 5px; padding: 20px; background: rgba(0,0,0,0.1); color: #ccc;">
|
||||
<span style="display: inline-block;font-weight: bold; margin-bottom: 8px;">⚠️ A note on hot wallets:</span><br>
|
||||
This will create a <a style="color: #ddd;" href="https://en.bitcoin.it/wiki/Hot_wallet" target="_blank">hot wallet</a> on your Bitcoin Core node.<br>
|
||||
Hot wallets are considered less secure and are not recommended for use with significant amounts. Use with caution and beware the potential risks.
|
||||
</p>
|
||||
<p>Enter you mnemonic here or use the randomly generated one:</p>
|
||||
<p><b>Make sure to backup these words!</b></p>
|
||||
{% set wordslist = mnemonic.split(' ') %}
|
||||
<nav class="row">
|
||||
<button type="button" id="generate_mnemonic_btn" class="btn radio left checked" onclick="setMnemonicView('generate')"> Generate </button>
|
||||
<button type="button" id="import_mnemonic_btn" class="btn radio right" onclick="setMnemonicView('import')"> Import </button>
|
||||
</nav>
|
||||
<input id="mnemonic_value" type="hidden" name="mnemonic" value="{{ mnemonic }}"/>
|
||||
<textarea id="import_mnemonic" class="hidden" id="txt" placeholder="Enter your mnemonic here"></textarea>
|
||||
<div id="generated_mnemonic">
|
||||
<div id="generated_mnemonic_row" class="row center break-row-mobile" style="max-height: 30px;">
|
||||
<fieldset style="border:none; display: inline;">
|
||||
<select name="strength" id="strength">
|
||||
<option value="128">12 words</option>
|
||||
<option value="256">24 words</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
<button type="button" class="btn" style="max-width: 200px; margin-top: 9px;" onclick="generateMnemonic()">Generate New Mnemonic</button>
|
||||
</div>
|
||||
<br>
|
||||
<table id="mnemonic_table">
|
||||
{% for i in range(wordslist|length // 4) %}
|
||||
<tr>
|
||||
<td id="mnemonicwo">{{ 4 * i + 1 }}. {{ wordslist[4 * i] }}</td>
|
||||
<td>{{ 4 * i + 2 }}. {{ wordslist[4 * i + 1] }}</td>
|
||||
<td>{{ 4 * i + 3 }}. {{ wordslist[4 * i + 2] }}</td>
|
||||
<td>{{ 4 * i + 4 }}. {{ wordslist[4 * i + 3] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div {% if not existing_device or existing_device.hot_wallet %}id="step2" class="hidden"{% else %}id="step1"{% endif %}>
|
||||
{% if device_class.hot_wallet %}
|
||||
<input type="hidden" name="mnemonic" value="{{ mnemonic }}"/>
|
||||
<input type="hidden" name="passphrase" value="{{ passphrase }}"/>
|
||||
<input type="hidden" name="file_password" value="{{ file_password }}"/>
|
||||
<input type="hidden" name="range_start" value="{{ range_start }}"/>
|
||||
<input type="hidden" name="range_end" value="{{ range_end }}"/>
|
||||
{% endif %}
|
||||
<div>
|
||||
<h1 style="font-size: 1.8em;">Upload Keys</h1>
|
||||
{% if not existing_device %}
|
||||
<h2 style="max-width: 500px; margin: auto;">Name your Device: <input type="text" name="device_name" placeholder="My device" id="device_name" required/></h2>
|
||||
<br>
|
||||
{% endif %}
|
||||
<div id="coldcard-instructions">
|
||||
<div id="coldcard-instructions" {%if device_class.device_type != 'coldcard' %}class="hidden"{% endif %}>
|
||||
<p>
|
||||
Connect your ColdCard to the computer via USB and unlock it or upload a wallet export file from micro SD card.<br>
|
||||
</p>
|
||||
|
|
@ -113,7 +61,7 @@
|
|||
To get the multisig xpubs, go to: Settings -> Multisig Wallets -> Export XPUB and confirm the export.
|
||||
</span>
|
||||
</div>
|
||||
<div id="specter-instructions">
|
||||
<div id="specter-instructions" {%if device_class.device_type != 'specter' %}class="hidden"{% endif %}>
|
||||
<p>
|
||||
Connect your Specter-DIY to the computer via USB and unlock it or scan the wallet master public key.<br>
|
||||
</p>
|
||||
|
|
@ -121,7 +69,7 @@
|
|||
To get the master public key QR code to scan, click on: Master public keys, then select each key type you'd like to import and scan the displayed QR code for each into Specter.
|
||||
</span>
|
||||
</div>
|
||||
<div id="cobo-instructions">
|
||||
<div id="cobo-instructions" {%if device_class.device_type != 'cobo' %}class="hidden"{% endif %}>
|
||||
<p>
|
||||
Scan your Cobo Vault master public key or upload a wallet export file.<br>
|
||||
</p>
|
||||
|
|
@ -133,7 +81,7 @@
|
|||
To import with SD card, click on "touch here to export the file with microSD" on the same screen as the QR code.
|
||||
</span>
|
||||
</div>
|
||||
<div id="hwi-only-instructions">
|
||||
<div id="hwi-only-instructions" {%if device_class.device_type in ['bitcoincore', 'cobo', 'coldcard', 'electrum', 'other', 'specter']%}class="hidden"{% endif %}>
|
||||
<p>Connect your hardware device to the computer via USB.</p>
|
||||
</div>
|
||||
<br>
|
||||
|
|
@ -149,26 +97,26 @@
|
|||
</table>
|
||||
<br>
|
||||
<div class="row overflow">
|
||||
<div id="connect-hwi" class="btn" style="max-width: 250px; width: 250px; margin: 10px;">
|
||||
<div id="connect-hwi" style="max-width: 250px; width: 250px; margin: 10px;" class="btn {% if not device_class.hwi_support %}hidden{% endif %}">
|
||||
<img src="{{ url_for('static', filename='img/usb.svg') }}" style="width: 26px; margin-right: 2px;" class="svg-white">
|
||||
Get via USB
|
||||
</div>
|
||||
<label id="connect-sdcard">
|
||||
<label id="connect-sdcard" {% if not device_class.sd_card_support %}class="hidden"{% endif %}>
|
||||
<input type="file" id="file" class="inputfile" multiple/>
|
||||
<div class="btn" style="max-width: 250px; width: 250px; margin: 10px;">
|
||||
<img src="{{ url_for('static', filename='img/sd-card.svg') }}" style="width: 26px; margin: 0px;" class="svg-white">
|
||||
Upload from SD Card
|
||||
</div>
|
||||
</label>
|
||||
<qr-scanner id="xpub-scan">
|
||||
<qr-scanner id="xpub-scan" {% if not device_class.qr_code_support %}class="hidden"{% endif %}>
|
||||
<a slot="button" href="#" class="btn" style="max-width: 250px; width: 250px; margin: 10px;">
|
||||
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 26px; margin: 0px;" class="svg-white">
|
||||
Scan QR code
|
||||
</a>
|
||||
</qr-scanner>
|
||||
<button type="button" onclick="showPageOverlay('paste-xpub-popup')" class="btn centered" id="xpub-paste" style="max-width: 250px; width: 250px; margin: 10px;">
|
||||
<img src="{{ url_for('static', filename='img/copy.svg') }}" style="width: 22px; margin: 0px;" class="svg-white">
|
||||
Paste xpub
|
||||
<button {% if device_class.device_type not in ['electrum', 'other'] %}class="hidden"{% endif %} type="button" onclick="showPageOverlay('paste-xpub-popup')" class="btn centered" id="xpub-paste" style="max-width: 250px; width: 250px; margin: 10px;">
|
||||
<img src="{{ url_for('static', filename='img/copy.svg') }}" style="width: 22px; margin: 0px;" class="svg-white">
|
||||
Paste xpub
|
||||
</button>
|
||||
<div class="hidden" id="paste-xpub-popup">
|
||||
<h1>Paste xpub</h1>
|
||||
|
|
@ -177,32 +125,11 @@
|
|||
<button type="button" onclick="addXpubs(document.getElementById('paste-xpub-text').value);hidePageOverlay();document.getElementById('paste-xpub-text').value='';" class="btn centered">Add xpub</button>
|
||||
</div>
|
||||
</div>
|
||||
<span id="toggle_advanced_xpubs" style="cursor: pointer;" onclick="toggleAdvancedXpubs()">Advanced ▶</span>
|
||||
<br>
|
||||
<div id="advanced_settings_xpubs" class="hidden">
|
||||
<div id="advanced-hot-wallet" class="hidden" style="width: 400px; margin: auto;">
|
||||
<label>Encrypt Wallet File: </label>
|
||||
<input type="password" name="file_password" class="inline" placeholder="password">
|
||||
<br>
|
||||
<br>
|
||||
<label>BIP39 passphrase (optional): </label>
|
||||
<input type="password" name="passphrase" class="inline" placeholder="passphrase">
|
||||
<br>
|
||||
<p style="margin-bottom: 10px;">Addresses range to import: </p>
|
||||
From: <input type="number" name="range_start" class="inline" placeholder="start" value=0 step=1 min=0 max=100000>
|
||||
to: <input type="number" name="range_end" class="inline" placeholder="start" value=1000 step=1 min=0 max=100000>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<button type="button" class="btn hidden" style="width: 140px; max-width: 140px; margin: auto; margin-right: 40px;" id="wizard-previous" onclick="prevStep()">Previous</button>
|
||||
<button type="button" class="btn" style="width: 140px; max-width: 140px; margin: auto;" id="wizard-next" onclick="nextStep()" disabled>Next</button>
|
||||
<button type="submit" class="btn centered action hidden" style="width: 140px; max-width: 140px; margin: auto;" id="wizard-submit">Add {% if existing_device %}Keys{% else %}Device{% endif %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" id="submit-keys" class="btn centered action" style="width: 140px; max-width: 140px; margin: auto;">Continue</button>
|
||||
<br><br>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
|
@ -346,265 +273,6 @@
|
|||
setupXpubsTable();
|
||||
});
|
||||
|
||||
function setMnemonicView(view) {
|
||||
var generatedMnemonic = document.getElementById("generated_mnemonic");
|
||||
var importMnemonic = document.getElementById("import_mnemonic");
|
||||
var generateMnemonicBtn = document.getElementById("generate_mnemonic_btn");
|
||||
var importMnemonicBtn = document.getElementById("import_mnemonic_btn");
|
||||
var mnemonicValue = document.getElementById("mnemonic_value");
|
||||
if (view == 'generate') {
|
||||
generatedMnemonic.style.display = 'block';
|
||||
importMnemonic.style.display = 'none';
|
||||
generateMnemonicBtn.className = generateMnemonicBtn.className += ' checked';
|
||||
importMnemonicBtn.className = importMnemonicBtn.className.replace(/checked/, '');
|
||||
mnemonicValue.value = '{{ mnemonic }}'
|
||||
} else {
|
||||
generatedMnemonic.style.display = 'none';
|
||||
importMnemonic.style.display = 'block';
|
||||
generateMnemonicBtn.className = generateMnemonicBtn.className.replace(/checked/, '');
|
||||
importMnemonicBtn.className = importMnemonicBtn.className += ' checked';
|
||||
mnemonicValue.value = importMnemonic.value;
|
||||
importMnemonic.addEventListener("input", function() {
|
||||
mnemonicValue.value = importMnemonic.value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function generateMnemonic() {
|
||||
let url = `{{ url_for('generatemnemonic') }}`;
|
||||
var formData = new FormData(document.getElementById('form'));
|
||||
try {
|
||||
const response = await fetch(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}
|
||||
);
|
||||
if(response.status != 200){
|
||||
showError(await response.text());
|
||||
return;
|
||||
}
|
||||
const jsonResponse = await response.json();
|
||||
let mnemonic = jsonResponse.mnemonic;
|
||||
|
||||
document.getElementById("mnemonic_value").value = mnemonic;
|
||||
let wordslist = mnemonic.split(' ');
|
||||
let mnemonicTable = document.getElementById("mnemonic_table");
|
||||
mnemonicTable.innerHTML = ``;
|
||||
|
||||
for(let i=0; i < wordslist.length / 4; i++) {
|
||||
mnemonicTable.innerHTML += `
|
||||
<tr>
|
||||
<td id="mnemonicwo">${ 4 * i + 1 }. ${ wordslist[4 * i] }</td>
|
||||
<td>${ 4 * i + 2 }. ${ wordslist[4 * i + 1] }</td>
|
||||
<td>${ 4 * i + 3 }. ${ wordslist[4 * i + 2] }</td>
|
||||
<td>${ 4 * i + 4 }. ${ wordslist[4 * i + 3] }</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
} catch(e) {
|
||||
console.log("Caught error: ", e);
|
||||
showError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Wizard JS
|
||||
|
||||
let currentStep = 1;
|
||||
let totalSteps = parseInt('{{ 1 if existing_device and not existing_device.hot_wallet else 2 }}');
|
||||
let stepsCompleted = [];
|
||||
{% if existing_device %}
|
||||
{% if existing_device.hot_wallet %}
|
||||
setStepCompleted(1);
|
||||
{% else %}
|
||||
nextStep();
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
function checkStepCompleted(step) {
|
||||
}
|
||||
|
||||
function setStepCompleted(step) {
|
||||
document.getElementById('wizard-next').removeAttribute('disabled');
|
||||
stepsCompleted.push(step);
|
||||
}
|
||||
|
||||
function setStepUncompleted(step) {
|
||||
document.getElementById('wizard-next').setAttribute('disabled', true);
|
||||
delete stepsCompleted[(stepsCompleted.indexOf(step))];
|
||||
}
|
||||
|
||||
function nextStep() {
|
||||
if (currentStep == 1) {
|
||||
{% if existing_device %}
|
||||
let device = {
|
||||
value: "{{ existing_device.device_type }}",
|
||||
'data-qrcode': "{{existing_device.qr_code_support}}",
|
||||
'data-sdcard': "{{existing_device.sd_card_support}}",
|
||||
'data-hwi': "{{existing_device.hwi_support}}",
|
||||
'data-device-name': "{{ existing_device.name }}"
|
||||
}
|
||||
{% else %}
|
||||
var devices = document.getElementsByName('devices');
|
||||
let device;
|
||||
for (var i = 0, length = devices.length; i < length; i++) {
|
||||
if (devices[i].checked) {
|
||||
device = devices[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
device = {
|
||||
value: device.value,
|
||||
'data-qrcode': device.getAttribute('data-qrcode'),
|
||||
'data-sdcard': device.getAttribute('data-sdcard'),
|
||||
'data-hwi': device.getAttribute('data-hwi'),
|
||||
'data-device-name': device.getAttribute('data-device-name')
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
if (device.value == 'bitcoincore') {
|
||||
if (document.getElementById('step2-hot-wallet')) {
|
||||
document.getElementById('step2').id = 'step3';
|
||||
document.getElementById('step2-hot-wallet').id = 'step2';
|
||||
}
|
||||
document.getElementById('toggle_advanced_xpubs').style.display = 'block';
|
||||
totalSteps = parseInt('{{ 2 if existing_device else 3 }}');
|
||||
} else {
|
||||
if (document.getElementById('step3')) {
|
||||
document.getElementById('step3').id = 'step2';
|
||||
document.getElementById('step2').id = 'step2-hot-wallet';
|
||||
}
|
||||
document.getElementById('toggle_advanced_xpubs').style.display = 'none';
|
||||
totalSteps = parseInt('{{ 1 if existing_device else 2 }}');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Update connect methods for step 2
|
||||
if (device['data-hwi'] == 'True') {
|
||||
document.getElementById('connect-hwi').classList.remove('hidden');
|
||||
} else {
|
||||
document.getElementById('connect-hwi').classList.add('hidden');
|
||||
}
|
||||
|
||||
if (device['data-sdcard'] == 'True') {
|
||||
document.getElementById('connect-sdcard').classList.remove('hidden');
|
||||
} else {
|
||||
document.getElementById('connect-sdcard').classList.add('hidden');
|
||||
}
|
||||
|
||||
if (device['data-qrcode'] == 'True') {
|
||||
document.getElementById('xpub-scan').classList.remove('hidden');
|
||||
} else {
|
||||
document.getElementById('xpub-scan').classList.add('hidden');
|
||||
}
|
||||
if (device.value == 'electrum' || device.value == 'other') {
|
||||
document.getElementById('xpub-paste').classList.remove('hidden');
|
||||
} else {
|
||||
document.getElementById('xpub-paste').classList.add('hidden');
|
||||
}
|
||||
document.getElementById('specter-instructions').classList.add('hidden');
|
||||
document.getElementById('coldcard-instructions').classList.add('hidden');
|
||||
document.getElementById('cobo-instructions').classList.add('hidden');
|
||||
document.getElementById('hot-wallet').classList.add('hidden');
|
||||
document.getElementById('advanced-hot-wallet').classList.add('hidden');
|
||||
document.getElementById('hwi-only-instructions').classList.add('hidden');
|
||||
switch(device.value) {
|
||||
case 'specter':
|
||||
document.getElementById('specter-instructions').classList.remove('hidden');
|
||||
break;
|
||||
case 'coldcard':
|
||||
document.getElementById('coldcard-instructions').classList.remove('hidden');
|
||||
break;
|
||||
case 'cobo':
|
||||
document.getElementById('cobo-instructions').classList.remove('hidden');
|
||||
break;
|
||||
case 'electrum':
|
||||
case 'other':
|
||||
break;
|
||||
case 'bitcoincore':
|
||||
document.getElementById('hot-wallet').classList.remove('hidden');
|
||||
document.getElementById('advanced-hot-wallet').classList.remove('hidden');
|
||||
break;
|
||||
default:
|
||||
document.getElementById('hwi-only-instructions').classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
if (currentStep < totalSteps) {
|
||||
document.getElementById('wizard-previous').classList.remove('hidden');
|
||||
document.getElementById(`step${currentStep}`).classList.add('hidden');
|
||||
document.getElementById(`step${currentStep + 1}`).classList.remove('hidden');
|
||||
if (!document.getElementById('step3')) { // Only bitcoin core hot wallet
|
||||
document.getElementById('wizard-next').setAttribute('disabled', 'true');
|
||||
}
|
||||
currentStep++;
|
||||
}
|
||||
|
||||
if (currentStep in stepsCompleted) {
|
||||
setStepCompleted(currentStep);
|
||||
}
|
||||
|
||||
if (currentStep == totalSteps) {
|
||||
document.getElementById('wizard-next').classList.add('hidden');
|
||||
document.getElementById('wizard-submit').classList.remove('hidden');
|
||||
|
||||
let deviceName;
|
||||
let deviceType;
|
||||
|
||||
var devices = document.getElementsByName('devices');
|
||||
|
||||
for (var i = 0, length = devices.length; i < length; i++) {
|
||||
if (devices[i].checked) {
|
||||
deviceName = devices[i]['data-device-name'];
|
||||
deviceType = devices[i].value;
|
||||
setupXpubsTable(deviceType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (deviceType == 'other' || deviceType == 'electrum') {
|
||||
xpubsEditable = true;
|
||||
} else {
|
||||
xpubsEditable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prevStep() {
|
||||
document.getElementById(`step${currentStep}`).classList.add('hidden');
|
||||
document.getElementById(`step${currentStep - 1}`).classList.remove('hidden');
|
||||
currentStep--;
|
||||
if (currentStep == 1) {
|
||||
document.getElementById('wizard-previous').classList.add('hidden');
|
||||
}
|
||||
setStepCompleted(currentStep);
|
||||
|
||||
|
||||
document.getElementById('wizard-next').classList.remove('hidden');
|
||||
document.getElementById('wizard-submit').classList.add('hidden');
|
||||
}
|
||||
|
||||
function filterDeviceTypes(text) {
|
||||
let devicesLabels = []
|
||||
{% for cls in specter.device_manager.supported_devices %}
|
||||
devicesLabels.push('label_device_type_{{ cls.name }}')
|
||||
{% endfor %}
|
||||
if (text) {
|
||||
for (let deviceLabel of devicesLabels) {
|
||||
if (deviceLabel.split('label_device_type_')[1].toLowerCase().includes(text.toLowerCase())) {
|
||||
document.getElementById(deviceLabel).style.display = 'flex';
|
||||
} else {
|
||||
document.getElementById(deviceLabel).style.display = 'none';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let deviceLabel of devicesLabels) {
|
||||
document.getElementById(deviceLabel).style.display = 'flex';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function editXpubsTable(editBtn) {
|
||||
let visibility = '';
|
||||
if (editBtn.innerHTML == 'Edit') {
|
||||
|
|
@ -621,23 +289,6 @@
|
|||
el.style.visibility = visibility;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAdvancedXpubs() {
|
||||
let advancedButton = document.getElementById('toggle_advanced_xpubs');
|
||||
let advancedSettings = document.getElementById('advanced_settings_xpubs');
|
||||
if (advancedSettings.style.display === 'block') {
|
||||
advancedSettings.style.display = 'none';
|
||||
advancedButton.innerHTML = 'Advanced ▶';
|
||||
} else {
|
||||
advancedSettings.style.display = 'block';
|
||||
advancedButton.innerHTML = 'Advanced ▼';
|
||||
if (totalSteps == 3) {
|
||||
document.getElementById('advanced-hot-wallet').display = 'block';
|
||||
} else {
|
||||
document.getElementById('advanced-hot-wallet').display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
{% extends "base.jinja" %}
|
||||
{% block main %}
|
||||
{% include "includes/hwi/hwi.jinja" %}
|
||||
{% include "includes/qr-scanner.html" %}
|
||||
<style>
|
||||
td {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media screen and (max-width:720px) {
|
||||
#device_setup_wizard th, #device_setup_wizard td {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
#device_setup_wizard td:empty, #device_setup_wizard th:empty {
|
||||
display: none;
|
||||
margin: auto;
|
||||
}
|
||||
#device_setup_wizard th, #device_setup_wizard td, #device_setup_wizard td > input {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
#device_setup_wizard .xpub {
|
||||
padding: 10px;
|
||||
}
|
||||
#device_setup_wizard #edit-xpubs-table-btn {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
.xpubs_edit {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
<form id="form" action="{{ url_for('devices_endpoint.new_device_mnemonic') }}" method="POST" style="width: 100%;" onsubmit="showPacman();">
|
||||
<div class="card center" style="width: auto; margin: 40px;" >
|
||||
<h1 style="font-size: 2em;">Add {% if existing_device %}Keys{% else %}Device{% endif %}</h1><br>
|
||||
{% if existing_device %}
|
||||
<input type="hidden" name="existing_device" value="{{ existing_device.alias }}">
|
||||
{% endif %}
|
||||
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div>
|
||||
<div id="hot-wallet" style="width: 80%; max-width: 700px; margin: auto;">
|
||||
<br>
|
||||
<p class="note" style="border-radius: 5px; padding: 20px; background: rgba(0,0,0,0.1); color: #ccc;">
|
||||
<span style="display: inline-block;font-weight: bold; margin-bottom: 8px;">⚠️ A note on hot wallets:</span><br>
|
||||
This will create a <a style="color: #ddd;" href="https://en.bitcoin.it/wiki/Hot_wallet" target="_blank">hot wallet</a> on your Bitcoin Core node.<br>
|
||||
Hot wallets are considered less secure and are not recommended for use with significant amounts. Use with caution and beware the potential risks.
|
||||
</p>
|
||||
<p>Enter your mnemonic here{% if not existing_device %} or use the randomly generated one{% endif %}:</p>
|
||||
{% if not existing_device %}<p><b>Make sure to backup these words!</b></p>{% endif %}
|
||||
{% set wordslist = mnemonic.split(' ') %}
|
||||
{% if not existing_device %}
|
||||
<nav class="row">
|
||||
<button type="button" id="generate_mnemonic_btn" class="btn radio left checked" onclick="setMnemonicView('generate')"> Generate </button>
|
||||
<button type="button" id="import_mnemonic_btn" class="btn radio right" onclick="setMnemonicView('import')"> Import </button>
|
||||
</nav>
|
||||
{% endif %}
|
||||
<input id="mnemonic_value" type="hidden" name="mnemonic" value="{{ mnemonic }}"/>
|
||||
<textarea id="import_mnemonic" class="hidden" id="txt" placeholder="Enter your mnemonic here"></textarea>
|
||||
<div id="generated_mnemonic" {% if existing_device %}class="hidden"{% endif %}>
|
||||
<div id="generated_mnemonic_row" class="row center break-row-mobile" style="max-height: 30px;">
|
||||
<fieldset style="border:none; display: inline;">
|
||||
<select name="strength" id="strength">
|
||||
<option value="128">12 words</option>
|
||||
<option value="256">24 words</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
<button type="button" class="btn" style="max-width: 200px; margin-top: 9px;" onclick="generateMnemonic()">Generate New Mnemonic</button>
|
||||
</div>
|
||||
<br>
|
||||
<table id="mnemonic_table">
|
||||
{% for i in range(wordslist|length // 4) %}
|
||||
<tr>
|
||||
<td id="mnemonicwo">{{ 4 * i + 1 }}. {{ wordslist[4 * i] }}</td>
|
||||
<td>{{ 4 * i + 2 }}. {{ wordslist[4 * i + 1] }}</td>
|
||||
<td>{{ 4 * i + 3 }}. {{ wordslist[4 * i + 2] }}</td>
|
||||
<td>{{ 4 * i + 4 }}. {{ wordslist[4 * i + 3] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<span id="toggle-advanced" style="cursor: pointer;" onclick="toggleAdvanced()">Advanced ▶</span>
|
||||
<br>
|
||||
<br>
|
||||
<div id="advanced-settings" class="hidden" style="width: 400px; margin: auto;">
|
||||
<label>Encrypt Wallet File: </label>
|
||||
<input type="password" name="file_password" class="inline" placeholder="password">
|
||||
<br>
|
||||
<br>
|
||||
<label>BIP39 passphrase (optional): </label>
|
||||
<input type="password" name="passphrase" class="inline" placeholder="passphrase">
|
||||
<br>
|
||||
<p style="margin-bottom: 10px;">Addresses range to import: </p>
|
||||
From: <input type="number" name="range_start" class="inline" placeholder="start" value=0 step=1 min=0 max=100000>
|
||||
to: <input type="number" name="range_end" class="inline" placeholder="start" value=1000 step=1 min=0 max=100000>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" id="submit-mnemonic" class="btn centered action" style="width: 140px; max-width: 140px; margin: auto;">Continue</button>
|
||||
<br><br>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function(){
|
||||
{% if existing_device %}
|
||||
setMnemonicView('import');
|
||||
{% endif %}
|
||||
});
|
||||
|
||||
function setMnemonicView(view) {
|
||||
var generatedMnemonic = document.getElementById("generated_mnemonic");
|
||||
var importMnemonic = document.getElementById("import_mnemonic");
|
||||
var generateMnemonicBtn = document.getElementById("generate_mnemonic_btn");
|
||||
var importMnemonicBtn = document.getElementById("import_mnemonic_btn");
|
||||
var mnemonicValue = document.getElementById("mnemonic_value");
|
||||
if (view == 'generate') {
|
||||
generatedMnemonic.style.display = 'block';
|
||||
importMnemonic.style.display = 'none';
|
||||
generateMnemonicBtn.className = generateMnemonicBtn.className += ' checked';
|
||||
importMnemonicBtn.className = importMnemonicBtn.className.replace(/checked/, '');
|
||||
mnemonicValue.value = '{{ mnemonic }}'
|
||||
} else {
|
||||
generatedMnemonic.style.display = 'none';
|
||||
importMnemonic.style.display = 'block';
|
||||
generateMnemonicBtn.className = generateMnemonicBtn.className.replace(/checked/, '');
|
||||
importMnemonicBtn.className = importMnemonicBtn.className += ' checked';
|
||||
mnemonicValue.value = importMnemonic.value;
|
||||
importMnemonic.addEventListener("input", function() {
|
||||
mnemonicValue.value = importMnemonic.value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function generateMnemonic() {
|
||||
let url = `{{ url_for('generatemnemonic') }}`;
|
||||
var formData = new FormData(document.getElementById('form'));
|
||||
try {
|
||||
const response = await fetch(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}
|
||||
);
|
||||
if(response.status != 200){
|
||||
showError(await response.text());
|
||||
return;
|
||||
}
|
||||
const jsonResponse = await response.json();
|
||||
let mnemonic = jsonResponse.mnemonic;
|
||||
|
||||
document.getElementById("mnemonic_value").value = mnemonic;
|
||||
let wordslist = mnemonic.split(' ');
|
||||
let mnemonicTable = document.getElementById("mnemonic_table");
|
||||
mnemonicTable.innerHTML = ``;
|
||||
|
||||
for(let i=0; i < wordslist.length / 4; i++) {
|
||||
mnemonicTable.innerHTML += `
|
||||
<tr>
|
||||
<td id="mnemonicwo">${ 4 * i + 1 }. ${ wordslist[4 * i] }</td>
|
||||
<td>${ 4 * i + 2 }. ${ wordslist[4 * i + 1] }</td>
|
||||
<td>${ 4 * i + 3 }. ${ wordslist[4 * i + 2] }</td>
|
||||
<td>${ 4 * i + 4 }. ${ wordslist[4 * i + 3] }</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
} catch(e) {
|
||||
console.log("Caught error: ", e);
|
||||
showError(e);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAdvanced() {
|
||||
let advancedButton = document.getElementById('toggle-advanced');
|
||||
let advancedSettings = document.getElementById('advanced-settings');
|
||||
if (advancedSettings.style.display === 'block') {
|
||||
advancedSettings.style.display = 'none';
|
||||
advancedButton.innerHTML = 'Advanced ▶';
|
||||
} else {
|
||||
advancedSettings.style.display = 'block';
|
||||
advancedButton.innerHTML = 'Advanced ▼';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
{% extends "base.jinja" %}
|
||||
{% block main %}
|
||||
<h1 style="font-size: 2em;">Add Device</h1><br>
|
||||
<div id="device_setup_wizard" class="card center" style="width: auto; 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)">
|
||||
<div id="devices_list" class="row overflow">
|
||||
{% for cls in specter.device_manager.supported_devices %}
|
||||
{% 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') }}" 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">
|
||||
{{ cls.name }}
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function filterDeviceTypes(text) {
|
||||
let devicesLabels = []
|
||||
{% for cls in specter.device_manager.supported_devices %}
|
||||
{% 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())) {
|
||||
document.getElementById(deviceLabel).style.display = 'flex';
|
||||
} else {
|
||||
document.getElementById(deviceLabel).style.display = 'none';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let deviceLabel of devicesLabels) {
|
||||
document.getElementById(deviceLabel).style.display = 'flex';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -93,11 +93,7 @@
|
|||
upub5En4f7k8gaG2KDHvBeEYox...rFpJRHpiZ4DE
|
||||
</pre>
|
||||
</div><br>
|
||||
{% if not device %}
|
||||
<button type="submit" class="btn centered" name="action" value="newcolddevice" onsubmit="showPacman()">Continue</button>
|
||||
{% else %}
|
||||
<button type="submit" class="btn centered" name="action" value="morekeys" onsubmit="showPacman()">Continue</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not device or device.hot_wallet %}
|
||||
|
|
@ -169,11 +165,7 @@ m/48h/{{ 0 if specter.info.chain == "main" else 1 }}h/0h/2h</textarea>
|
|||
<br>
|
||||
</div>
|
||||
<br><br>
|
||||
{% if not device %}
|
||||
<button type="submit" class="btn centered" name="action" value="newhotdevice">Continue</button>
|
||||
{% else %}
|
||||
<button type="submit" class="btn centered" name="action" value="morekeys">Continue</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{#
|
||||
sidebar_btn - Add button used in the sidebar for wallets and devices.
|
||||
Parameters:
|
||||
- url_path: URL path the button should lead to (ie. 'new_wallet', 'new_device').
|
||||
- url_path: URL path the button should lead to (ie. 'new_wallet', 'new_device_type').
|
||||
- text: The text of the button.
|
||||
- id: an optional id for the "a" element
|
||||
- icon: should show an icon of +
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@
|
|||
{% for device_name in specter.device_manager.devices_names %}
|
||||
{{ sidebar_device_list_item(specter.device_manager.devices[device_name], device_alias) }}
|
||||
{% endfor %}
|
||||
{{ sidebar_btn(url_for('devices_endpoint.new_device'), 'Add new device','btn_new_device') }}
|
||||
{{ sidebar_btn(url_for('devices_endpoint.new_device_type'), 'Add new device','btn_new_device') }}
|
||||
</div>
|
||||
<br>
|
||||
<div class="footer">
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
<a href="{{ url_for('devices_endpoint.new_device') }}" style="text-decoration: none; color: #fff;">
|
||||
<a href="{{ url_for('devices_endpoint.new_device_type') }}" style="text-decoration: none; color: #fff;">
|
||||
<div class="small-card radio">
|
||||
<img src="{{ url_for('static', filename='img/plus.svg') }}" style="transform: scale(1.5);" class="svg-white">
|
||||
Add new device
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ def test_home(caplog, client):
|
|||
assert result.status_code == 302 # REDIRECT.
|
||||
result = client.get("/about")
|
||||
assert b"Welcome to Specter" in result.data
|
||||
result = client.get("/devices/new_device", follow_redirects=True)
|
||||
result = client.get("/devices/new_device_type", follow_redirects=True)
|
||||
assert result.status_code == 200 # OK.
|
||||
assert b"Add Device" in result.data
|
||||
result = client.get("/settings", follow_redirects=True)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue