mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
Merge branch 'master' into favicon-for-light-and-dark-mode
This commit is contained in:
commit
801e3aa4a8
41 changed files with 502 additions and 304 deletions
|
|
@ -46,7 +46,6 @@ describe('Connecting nodes', () => {
|
|||
}
|
||||
else {
|
||||
cy.log('Test was skipped: Elementsd was not started by the test script.')
|
||||
Cypress.runner.stop()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ describe('Test adding different devices', () => {
|
|||
})
|
||||
|
||||
it('Validity of device name', () => {
|
||||
cy.get('#toggle_devices_list').click()
|
||||
cy.get('#devices_toggle').click()
|
||||
cy.get('#btn_new_device').click()
|
||||
cy.get('#trezor_device_card').click()
|
||||
cy.get("#device_name").type("'invalid")
|
||||
|
|
@ -16,7 +16,7 @@ describe('Test adding different devices', () => {
|
|||
});
|
||||
|
||||
it('Filter devices', () => {
|
||||
cy.get('#toggle_devices_list').click()
|
||||
cy.get('#devices_toggle').click()
|
||||
cy.get('#btn_new_device').click()
|
||||
cy.contains('Select your signing device')
|
||||
cy.get('#device-type-searchbar').type("Specter")
|
||||
|
|
@ -27,7 +27,7 @@ describe('Test adding different devices', () => {
|
|||
})
|
||||
|
||||
it('Electrum device', () => {
|
||||
cy.get('#toggle_devices_list').click()
|
||||
cy.get('#devices_toggle').click()
|
||||
cy.get('#btn_new_device').click()
|
||||
cy.get('#electrum_device_card').click()
|
||||
cy.get('#device_name').type("New Electrum Device")
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ describe('Test plugins', () => {
|
|||
cy.get('main').contains('Addresses').click()
|
||||
// Click on the first address
|
||||
cy.contains('td', '#0').siblings().contains('bcrt').click()
|
||||
cy.get('#associate-btn').click()
|
||||
cy.get('[data-cy="associate-address-with-service-btn"]').click()
|
||||
cy.contains("Service integration requires an authentication method that includes a password")
|
||||
// OK, let's set a password
|
||||
cy.get('select').select("passwordonly")
|
||||
|
|
|
|||
|
|
@ -14,9 +14,8 @@ describe('Test QR code signing flow', () => {
|
|||
cy.get('#messageDerivationPath').should('have.value', 'm/84h/1h/0h/0/0')
|
||||
cy.get('#message').type('The DIY is the best signing device.')
|
||||
cy.get('#diy_ghost_qr_sign_msg_btn').click()
|
||||
cy.get('#diy_ghost_sign_msg_qr > h2').contains('Scan this QR code')
|
||||
// To close the overlay (no cancel button here)
|
||||
cy.get('#page_overlay_popup').click()
|
||||
cy.get('[data-cy="scan-qr-code-for-msg-signing-overlay-headline"]').contains('Scan this QR code')
|
||||
cy.get('[data-cy="scan-qr-code-for-msg-signing-overlay-close-btn"]').click()
|
||||
})
|
||||
|
||||
it('No QR message signing button for a Trezor', () => {
|
||||
|
|
@ -27,14 +26,14 @@ describe('Test QR code signing flow', () => {
|
|||
// Only USB signing available for a Trezor device
|
||||
cy.contains('Sign message via USB').should('be.visible')
|
||||
cy.contains('Sign message via QR code').should('not.exist')
|
||||
cy.get('#page_overlay_popup').click()
|
||||
cy.get('[data-cy="close-msg-signing-overlay-btn"]').click()
|
||||
cy.changeDeviceType("DIY ghost", "specter")
|
||||
})
|
||||
|
||||
it('No message signing with Electrum', () => {
|
||||
cy.get('body').then(($body) => {
|
||||
if (!$body.text().includes("Electrum Device")) {
|
||||
cy.get('#toggle_devices_list').click()
|
||||
cy.get('#devices_toggle').click()
|
||||
cy.get('#btn_new_device').click()
|
||||
cy.get('#electrum_device_card').click()
|
||||
cy.get('#device_name').type("Electrum Device")
|
||||
|
|
@ -64,4 +63,4 @@ describe('Test QR code signing flow', () => {
|
|||
cy.deleteWallet("Wallet that cannot sign messages")
|
||||
cy.deleteDevice("Electrum Device")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -29,13 +29,13 @@ import 'cypress-wait-until';
|
|||
Cypress.Commands.add("addDevice", (name, device_type, mnemonic) => {
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.text().includes(name)) {
|
||||
cy.get('#toggle_devices_list').click()
|
||||
cy.get('#devices_toggle').click()
|
||||
cy.contains(name).click()
|
||||
return
|
||||
}
|
||||
cy.get('#side-content').click()
|
||||
if (!cy.get('#btn_new_device').isVisible) {
|
||||
cy.get('#toggle_devices_list').click()
|
||||
cy.get('#devices_toggle').click()
|
||||
}
|
||||
cy.get('#btn_new_device').click()
|
||||
cy.contains('Manual configuration').click()
|
||||
|
|
@ -66,7 +66,7 @@ Cypress.Commands.add("addHotDevice", (name, node_type) => {
|
|||
}
|
||||
cy.get('#side-content').click()
|
||||
if (!cy.get('#btn_new_device').isVisible) {
|
||||
cy.get('#toggle_devices_list').click()
|
||||
cy.get('#devices_toggle').click()
|
||||
}
|
||||
cy.get('#btn_new_device').click( {force: true} )
|
||||
cy.get(`#${node_type}core_device_card`).click()
|
||||
|
|
@ -80,7 +80,7 @@ Cypress.Commands.add("addHotDevice", (name, node_type) => {
|
|||
Cypress.Commands.add("deleteDevice", (name) => {
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.text().includes(name)) {
|
||||
cy.get('#toggle_devices_list').click()
|
||||
cy.get('#devices_toggle').click()
|
||||
cy.contains(name).click( {force: true} )
|
||||
cy.get('#forget_device').click()
|
||||
cy.reload()
|
||||
|
|
@ -94,9 +94,9 @@ Cypress.Commands.add("deleteDevice", (name) => {
|
|||
Cypress.Commands.add("changeDeviceType", (deviceName, newType) => {
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.text().includes(deviceName)) {
|
||||
cy.get('#toggle_devices_list').then(($devicesList) => {
|
||||
cy.get('#devices_toggle').then(($devicesList) => {
|
||||
if ($devicesList.text() == 'Devices ▸') {
|
||||
cy.get('#toggle_devices_list').click()
|
||||
cy.get('#devices_toggle').click()
|
||||
}
|
||||
})
|
||||
let refDeviceName = deviceName.toLowerCase().replace(/ /g,"_");
|
||||
|
|
@ -208,10 +208,10 @@ Cypress.Commands.add("deleteWallet", (walletName) => {
|
|||
Cypress.Commands.add("selectWallet", (walletName) => {
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.text().includes(walletName)) {
|
||||
cy.get('#toggle_wallets_list').then(($walletsList) => {
|
||||
cy.get('#wallets_toggle').then(($walletsList) => {
|
||||
// Only toggle the wallets list if it is not already toggled
|
||||
if ($walletsList.text() == 'Wallets ▸') {
|
||||
cy.get('#toggle_wallets_list').click()
|
||||
cy.get('#wallets_toggle').click()
|
||||
}
|
||||
cy.get(`[data-cy='wallet-sidebar-btn-${walletName}']`).click()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,26 +1,61 @@
|
|||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const crypto = require('crypto')
|
||||
const readLastLines = require('read-last-lines');
|
||||
const downloadloc = require('./downloadloc');
|
||||
const { loggers } = require('winston');
|
||||
const appName = downloadloc.appName()
|
||||
const appNameLower = appName.toLowerCase()
|
||||
const isDev = process.env.NODE_ENV === "development"
|
||||
const unresolvedDevFolder = process.env.SPECTER_DATA_FOLDER || "~/.specter_dev"
|
||||
const devFolder = unresolvedDevFolder.replace(/^~/, os.homedir());
|
||||
const prodFolder = path.resolve(os.homedir(), `.${appNameLower}`)
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
||||
let appSettingsPath
|
||||
let specterdDirPath
|
||||
let specterAppLogPath
|
||||
let versionData
|
||||
try {
|
||||
|
||||
// Use different version-data.jsons
|
||||
|
||||
// Should look like this:
|
||||
// {
|
||||
// "version": "v2.0.0-pre32",
|
||||
// "sha256": "aa049abf3e75199bad26fbded08ee5911ad48e325b42c43ec195136bd0736785"
|
||||
// }
|
||||
|
||||
if (isDev) {
|
||||
let versionDataPath = `${devFolder}/version-data.json`
|
||||
try {
|
||||
versionData = require(versionDataPath)
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
else {
|
||||
try {
|
||||
versionData = require('./version-data.json')
|
||||
console.log(versionData)
|
||||
} catch (e) {
|
||||
console.log('Could not find default version data configurations...'+e)
|
||||
}
|
||||
catch (e) {
|
||||
console.log('Could not find default version data configurations: '+e)
|
||||
versionData = {
|
||||
"version": "",
|
||||
"sha256": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
appSettingsPath = `${devFolder}/app_settings.json`
|
||||
specterdDirPath = `${devFolder}/specterd-binaries`
|
||||
specterAppLogPath = `${devFolder}/specterApp.log`
|
||||
}
|
||||
else {
|
||||
appSettingsPath = `${prodFolder}/app_settings.json`
|
||||
specterdDirPath = `${prodFolder}/specterd-binaries`
|
||||
specterAppLogPath = `${prodFolder}/specterApp.log`
|
||||
}
|
||||
const appSettingsPath = path.resolve(require('os').homedir(), `.${appNameLower}/app_settings.json`)
|
||||
const specterdDirPath = path.resolve(require('os').homedir(), `.${appNameLower}/specterd-binaries`)
|
||||
const specterAppLogPath = path.resolve(require('os').homedir(), `.${appNameLower}/specterApp.log`)
|
||||
|
||||
function getFileHash(filename, callback) {
|
||||
let shasum = crypto.createHash('sha256')
|
||||
|
|
@ -45,8 +80,8 @@ function getAppSettings() {
|
|||
basicAuthPass: '',
|
||||
tor: false,
|
||||
proxyURL: "socks5://127.0.0.1:9050",
|
||||
specterdVersion: versionData.version,
|
||||
specterdHash: versionData.sha256,
|
||||
specterdVersion: (versionData && versionData.version !== undefined) ? versionData.version : 'unknown',
|
||||
specterdHash: (versionData && versionData.sha256 !== undefined) ? versionData.sha256 : 'unknown',
|
||||
specterdCLIArgs: "",
|
||||
versionInitialized: false
|
||||
}
|
||||
|
|
@ -82,5 +117,9 @@ module.exports = {
|
|||
getAppSettings: getAppSettings,
|
||||
specterdDirPath: specterdDirPath,
|
||||
getSpecterAppLogs: getSpecterAppLogs,
|
||||
specterAppLogPath: specterAppLogPath
|
||||
specterAppLogPath: specterAppLogPath,
|
||||
versionData,
|
||||
isDev: isDev,
|
||||
devFolder,
|
||||
isMac, isMac
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,34 +9,30 @@ const extract = require('extract-zip')
|
|||
const defaultMenu = require('electron-default-menu');
|
||||
const ProgressBar = require('electron-progressbar')
|
||||
const { spawn, exec } = require('child_process');
|
||||
|
||||
const helpers = require('./helpers')
|
||||
const getFileHash = helpers.getFileHash
|
||||
const getAppSettings = helpers.getAppSettings
|
||||
const appSettingsPath = helpers.appSettingsPath
|
||||
const specterdDirPath = helpers.specterdDirPath
|
||||
|
||||
const { getFileHash, getAppSettings, appSettingsPath, specterdDirPath, specterAppLogPath, versionData, isDev, devFolder, isMac } = require('./helpers')
|
||||
const downloadloc = require('./downloadloc')
|
||||
const getDownloadLocation = downloadloc.getDownloadLocation
|
||||
const appName = downloadloc.appName()
|
||||
const appNameLower = appName.toLowerCase()
|
||||
|
||||
// Quit again if there is no version-data in dev
|
||||
if (isDev && versionData === undefined) {
|
||||
console.log(`You need to create a version-data.json in your dev folder (${devFolder}) to run the app. Check helpers.js for the format. Quitting ...`)
|
||||
app.quit()
|
||||
return
|
||||
}
|
||||
|
||||
ipcMain.handle("showMessageBoxSync", (e, message, buttons) => {
|
||||
dialog.showMessageBoxSync(mainWindow, { message, buttons });
|
||||
});
|
||||
|
||||
// Helper
|
||||
const isMac = process.platform === 'darwin'
|
||||
const isDev = process.env.NODE_ENV !== "production"
|
||||
|
||||
// Logging
|
||||
const {transports, format, createLogger } = require('winston')
|
||||
const combinedLog = new transports.File({ filename: helpers.specterAppLogPath });
|
||||
const combinedLog = new transports.File({ filename: specterAppLogPath });
|
||||
const winstonOptions = {
|
||||
exitOnError: false,
|
||||
format: format.combine(
|
||||
format.timestamp(),
|
||||
// format.timestamp({format:'MM/DD/YYYY hh:mm:ss.SSS'}),
|
||||
format.json(),
|
||||
format.printf(info => {
|
||||
return `${info.timestamp} [${info.level}] : ${info.message}`;
|
||||
|
|
@ -326,19 +322,6 @@ app.whenReady().then(() => {
|
|||
fs.mkdirSync(specterdDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
let versionData
|
||||
// Download specterd, run sha256sum on it and then set your own versionData in dev, should look this:
|
||||
// {
|
||||
// "version": "v2.0.0-pre32",
|
||||
// "sha256": "aa049abf3e75199bad26fbded08ee5911ad48e325b42c43ec195136bd0736785"
|
||||
// }
|
||||
if (isDev) {
|
||||
versionData = require('./version-data-dev.json')
|
||||
}
|
||||
else {
|
||||
versionData = require('./version-data.json')
|
||||
}
|
||||
|
||||
if (!appSettings.versionInitialized || appSettings.versionInitialized != versionData.version) {
|
||||
logger.info(`Updating ${appSettingsPath} : ${JSON.stringify(appSettings)}`);
|
||||
appSettings.specterdVersion = versionData.version
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
"version": "v0.0.0",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "cp -R ../../src/cryptoadvance/specter/static/fonts ../../src/cryptoadvance/specter/static/output.css ../../src/cryptoadvance/specter/static/typography.css . && electron .",
|
||||
"start": "export NODE_ENV=development && cp -R ../../src/cryptoadvance/specter/static/fonts ../../src/cryptoadvance/specter/static/output.css ../../src/cryptoadvance/specter/static/typography.css . && electron .",
|
||||
"pack": "electron-builder --dir",
|
||||
"dist": "export NODE_ENV=production && electron-builder --publish=never",
|
||||
"dist": "electron-builder --publish=never",
|
||||
"postinstall": "electron-builder install-app-deps"
|
||||
},
|
||||
"repository": "https://github.com/cryptoadvance/specter-desktop",
|
||||
|
|
@ -53,4 +53,4 @@
|
|||
"request": "^2.88.2",
|
||||
"winston": "^3.3.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"version": "v2.0.0-pre32",
|
||||
"sha256": "aa049abf3e75199bad26fbded08ee5911ad48e325b42c43ec195136bd0736785"
|
||||
}
|
||||
|
||||
|
|
@ -112,12 +112,17 @@ def to_ascii20(name: str) -> str:
|
|||
return "".join([c for c in name if ord(c) < 127])[:20]
|
||||
|
||||
|
||||
# TODO: Rename this function to sth. like create_unique_id
|
||||
def alias(name):
|
||||
"""
|
||||
Create a filesystem-friendly alias from a string.
|
||||
Replaces space with _ and keeps only alphanumeric chars.
|
||||
Leading and trailing whitespaces are removed.
|
||||
Replaces space(s) and hyphen(s) with one underscore.
|
||||
Keeps only alphanumeric chars and returns in lowercase.
|
||||
"""
|
||||
name = name.replace(" ", "_")
|
||||
name = name.strip().replace(" ", "_").replace("-", "_")
|
||||
while "__" in name:
|
||||
name = name.replace("__", "_")
|
||||
return "".join(x for x in name if x.isalnum() or x == "_").lower()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import sys
|
|||
import pathlib
|
||||
import sys
|
||||
|
||||
from typing import Dict
|
||||
from typing import Dict, List
|
||||
from flask_babel import lazy_gettext as _
|
||||
from flask import copy_current_request_context
|
||||
from cryptoadvance.specter.rpc import BitcoinRPC
|
||||
|
|
@ -298,9 +298,13 @@ class WalletManager:
|
|||
return working_folder
|
||||
|
||||
@property
|
||||
def wallets_names(self):
|
||||
def wallets_names(self) -> List:
|
||||
return sorted(self.wallets.keys())
|
||||
|
||||
@property
|
||||
def wallets_aliases(self) -> List:
|
||||
return [wallet.alias for wallet in self.wallets.values()]
|
||||
|
||||
@property
|
||||
def rpc(self):
|
||||
"""returns a BitcoinRpc depending on the chain"""
|
||||
|
|
@ -354,14 +358,6 @@ class WalletManager:
|
|||
walletsindir = []
|
||||
self._check_duplicate_keys(keys)
|
||||
wallet_alias = alias(name)
|
||||
i = 2
|
||||
# Ensure unique wallet alias
|
||||
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
|
||||
|
||||
w = self.WalletClass.create(
|
||||
self.rpc,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from flask_babel import lazy_gettext as _
|
|||
from flask_login import login_required
|
||||
|
||||
from ...commands.psbt_creator import PsbtCreator
|
||||
from ...helpers import bcur2base64, get_devices_with_keys_by_type, get_txid
|
||||
from ...helpers import bcur2base64, get_devices_with_keys_by_type, get_txid, alias
|
||||
from ...key import Key
|
||||
from ...managers.wallet_manager import purposes
|
||||
from ...persistence import delete_file
|
||||
|
|
@ -231,13 +231,6 @@ def new_wallet(wallet_type):
|
|||
).format(device.name)
|
||||
break
|
||||
|
||||
name = wallet_type.title()
|
||||
wallet_name = name
|
||||
i = 2
|
||||
while wallet_name in app.specter.wallet_manager.wallets_names:
|
||||
wallet_name = "%s %d" % (name, i)
|
||||
i += 1
|
||||
|
||||
return render_template(
|
||||
"wallet/new_wallet/new_wallet_keys.jinja",
|
||||
cosigners=devices,
|
||||
|
|
@ -253,8 +246,8 @@ def new_wallet(wallet_type):
|
|||
address_type = request.form["type"]
|
||||
sigs_total = int(request.form.get("sigs_total", 1))
|
||||
sigs_required = int(request.form.get("sigs_required", 1))
|
||||
if wallet_name in app.specter.wallet_manager.wallets_names:
|
||||
err = _("Wallet already exists")
|
||||
if alias(wallet_name) in app.specter.wallet_manager.wallets_aliases:
|
||||
err = _("Wallet name already exists. Choose a different name!")
|
||||
if err:
|
||||
devices = [
|
||||
app.specter.device_manager.get_by_alias(
|
||||
|
|
@ -805,8 +798,10 @@ def settings(wallet_alias):
|
|||
flash(_("Wallet name cannot be empty"), "error")
|
||||
elif wallet_name == wallet.name:
|
||||
pass
|
||||
elif wallet_name in app.specter.wallet_manager.wallets_names:
|
||||
flash(_("Wallet already exists"), "error")
|
||||
elif alias(wallet_name) in app.specter.wallet_manager.wallets_aliases:
|
||||
flash(
|
||||
_("Wallet name already exists. Choose a different name!"), "error"
|
||||
)
|
||||
else:
|
||||
app.specter.wallet_manager.rename_wallet(wallet, wallet_name)
|
||||
flash("Wallet successfully renamed!")
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@
|
|||
- isRight: Is the item at the right edge (last item) of the navigation bar?
|
||||
#}
|
||||
{% macro menu_item(service_id, tab, title, active_menuitem, isLeft=false, isRight=false) -%}
|
||||
<a
|
||||
href="{{ url_for(service_id+'_endpoint.' ~ tab)}}"
|
||||
class="btn radio {% if isLeft %}left{% endif %} {% if isRight %}right{% endif %} {% if active_menuitem == tab %}checked{% endif %}">
|
||||
{{ title }}
|
||||
</a>
|
||||
<li class="mr-2 py-2 border-b-2 border-transparent {% if active_menuitem == tab %}text-link border-b-2 border-link active{% endif %}">
|
||||
<a
|
||||
href="{{ url_for(service_id+'_endpoint.' ~ tab)}}"
|
||||
class="text-lg inline-block py-1 px-3 hover:text-white rounded-lg hover:bg-dark-700">
|
||||
{{ title }}
|
||||
</a>
|
||||
</li>
|
||||
{%- endmacro %}
|
||||
|
|
|
|||
|
|
@ -1239,6 +1239,10 @@ input[type="number"]::-webkit-outer-spin-button,
|
|||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.ml-5 {
|
||||
margin-left: 1.25rem;
|
||||
}
|
||||
|
||||
.ml-6 {
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
|
@ -1295,10 +1299,6 @@ input[type="number"]::-webkit-outer-spin-button,
|
|||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.mt-8 {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
|
@ -1307,10 +1307,6 @@ input[type="number"]::-webkit-outer-spin-button,
|
|||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -1523,6 +1519,10 @@ input[type="number"]::-webkit-outer-spin-button,
|
|||
min-width: 300px;
|
||||
}
|
||||
|
||||
.max-w-\[1100px\] {
|
||||
max-width: 1100px;
|
||||
}
|
||||
|
||||
.max-w-\[40px\] {
|
||||
max-width: 40px;
|
||||
}
|
||||
|
|
@ -1535,10 +1535,6 @@ input[type="number"]::-webkit-outer-spin-button,
|
|||
max-width: 65ch;
|
||||
}
|
||||
|
||||
.max-w-\[1100px\] {
|
||||
max-width: 1100px;
|
||||
}
|
||||
|
||||
.flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
@ -1571,6 +1567,11 @@ input[type="number"]::-webkit-outer-spin-button,
|
|||
flex-grow: 3;
|
||||
}
|
||||
|
||||
.rotate-90 {
|
||||
--tw-rotate: 90deg;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.transform {
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
|
@ -1694,6 +1695,12 @@ input[type="number"]::-webkit-outer-spin-button,
|
|||
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.space-x-5 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(1.25rem * var(--tw-space-x-reverse));
|
||||
margin-left: calc(1.25rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.space-y-1 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
|
|
@ -1746,10 +1753,6 @@ input[type="number"]::-webkit-outer-spin-button,
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.break-words {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.break-all {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
|
@ -2052,6 +2055,10 @@ input[type="number"]::-webkit-outer-spin-button,
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@
|
|||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if error %}
|
||||
<message-box type="error">{{ _("ERROR:") }} {{error | safe }}</message-box>
|
||||
<message-box type="error">{{error | safe }}</message-box>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -74,23 +74,38 @@
|
|||
|
||||
{% if device.hwi_support or device.supports_qr_message_signing %}
|
||||
<button class="button" type="button" onclick="showPageOverlay('message-signing')">{{ _("Sign message") }}</button>
|
||||
<div id="message-signing" class="hidden" >
|
||||
|
||||
<div id="message-signing" class="hidden p-4 bg-dark-800">
|
||||
<div class="flex justify-between mb-3">
|
||||
<h3 class="mb-0">{{ _("Message Signing") }}</h3>
|
||||
<p class="m-0 cursor-pointer" onclick="hidePageOverlay()" class="cursor-pointer" data-cy="close-msg-signing-overlay-btn">Close</p>
|
||||
</div>
|
||||
{% if origin %}
|
||||
{{ _("Address") }}:<input type="text" id="signing-address" type="text" value="" placeholder="bc1q ..."><br><br>
|
||||
<div class="floating-wrapper">
|
||||
<input class="floating-input peer" type="text" id="signing-address" type="text" value="" placeholder=" ">
|
||||
<label for="signing-address" class="floating-label">Address</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ _("Derivation path") }}:<input type="text" id="messageDerivationPath" type="text" value="" placeholder="e.g. m/84h/0h/0h/0/0"><br><br>
|
||||
{{ _("Message") }}:<br>
|
||||
<textarea id="message" placeholder="Enter the message you would like to sign"></textarea>
|
||||
|
||||
<div class="floating-wrapper">
|
||||
<input class="floating-input peer" type="text" id="messageDerivationPath" type="text" value="" placeholder=" ">
|
||||
<label class="floating-label">{{ _("Derivation path (e.g. m/84h/0h/0h/0/0)") }}</label>
|
||||
</div>
|
||||
|
||||
<textarea class="my-3" id="message" placeholder="Enter the message you would like to sign"></textarea>
|
||||
|
||||
<span id="signature-label" class="hidden">{{ _("Message signature") }}:</span>
|
||||
<p id="signature" title='{{ _("Copy signature") }}' class="hidden" onclick="copyText(this.innerText, '{{ _("Copied message signature") }}')">
|
||||
</p>
|
||||
<p id="signature" title='{{ _("Copy signature") }}' class="hidden" onclick="copyText(this.innerText, '{{ _("Copied message signature") }}')"></p>
|
||||
|
||||
{% if device.hwi_support %}
|
||||
<button class="button" id="{{ device.alias }}_usb_sign_msg_btn" type="button" onclick="signMessageOnDevice();">{{ _("Sign message via USB") }}</button>
|
||||
<button class="button mb-0 bg-accent" id="{{ device.alias }}_usb_sign_msg_btn" type="button" onclick="signMessageOnDevice();">{{ _("Sign message via USB") }}</button>
|
||||
{% endif %}
|
||||
|
||||
{% if device.supports_qr_message_signing %}
|
||||
{% include "includes/overlay/qr_code_sign_message.jinja" %}
|
||||
<button class="button" id="{{ device.alias }}_qr_sign_msg_btn">{{ _("Sign message via QR code") }}</button>
|
||||
<button class="button mb-0 mt-3 bg-accent" id="{{ device.alias }}_qr_sign_msg_btn">{{ _("Sign message via QR code") }}</button>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
|||
|
|
@ -122,11 +122,16 @@
|
|||
{{ _("Paste xpub") }}
|
||||
</button>
|
||||
|
||||
<div id="paste-xpub-popup" class="hidden">
|
||||
<h1>{{ _("Paste xpub") }}</h1>
|
||||
<input type="text" id="paste-xpub-text" placeholder="Paste your xpub here">
|
||||
<br><br>
|
||||
<button type="button" onclick="addXpubs(document.getElementById('paste-xpub-text').value);hidePageOverlay();document.getElementById('paste-xpub-text').value='';">{{ _("Add xpub") }}</button>
|
||||
<div id="paste-xpub-popup" class="hidden bg-dark-800 p-4 w-96">
|
||||
<div class="flex justify-between mb-2">
|
||||
<h3 class="mb-0">{{ _("Paste xpub") }}</h3>
|
||||
<p class="m-0 cursor-pointer" onclick="hidePageOverlay()" class="cursor-pointer">Close</p>
|
||||
</div>
|
||||
<div class="floating-wrapper mb-3 mt-1">
|
||||
<input class="floating-input peer" type="text" id="paste-xpub-text" placeholder=" ">
|
||||
<label class="floating-label" for="paste-xpub-text">xpub</label>
|
||||
</div>
|
||||
<button class="button bg-accent mr-0 mb-1 w-full" type="button" onclick="addXpubs(document.getElementById('paste-xpub-text').value);hidePageOverlay();document.getElementById('paste-xpub-text').value='';">{{ _("Add xpub") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,27 +19,6 @@
|
|||
height: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.service-btn, .signing-btn {
|
||||
background: var(--cmap-border);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
padding: 8px 15px 8px 10px;
|
||||
color: #fff;
|
||||
font-size: 1em;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
min-height: 33px;
|
||||
}
|
||||
.signing-btn {
|
||||
max-width: 315px;
|
||||
}
|
||||
.service-btn:hover, .signing-btn:hover {
|
||||
border: 1px solid var(--main-color);
|
||||
background: var(--cmap-border);
|
||||
}
|
||||
</style>
|
||||
<div class="address-data bg-dark-800 rounded-xl p-4">
|
||||
<div class="flex justify-between mb-2">
|
||||
|
|
@ -119,7 +98,7 @@
|
|||
addressInfoHTML += `<tr><td>{{ _("Service") }}:</td><td><img class="service-icon" src='{{ext_url_prefix}}/${this.serviceId}/static/${services[this.serviceId].icon}'>${services[this.serviceId].name}</td></tr>`;
|
||||
} else {
|
||||
let associateServiceUrl = `{{ url_for('services_endpoint.associate_addr', wallet_alias='WALLET_ALIAS', address='ADDRESS') }}`.replace('WALLET_ALIAS', this.wallet).replace('ADDRESS', jsonResponse.address);
|
||||
addressInfoHTML += `<tr><td>{{ _("Service") }}:</td><td><button type="button" id="associate-btn" class="service-btn" onclick="location.href='${associateServiceUrl}';">📥 {{ _("Associate with a service") }}</button></td></tr>`;
|
||||
addressInfoHTML += `<tr><td>{{ _("Service") }}:</td><td><button type="button" id="associate-btn" class="btn" onclick="location.href='${associateServiceUrl}';" data-cy="associate-address-with-service-btn">📥 {{ _("Associate with a service") }}</button></td></tr>`;
|
||||
};
|
||||
|
||||
// Message signing feature restricted to singlesig wallets using a Specter DIY
|
||||
|
|
@ -127,7 +106,7 @@
|
|||
{% if wallet.is_singlesig and device.hwi_support or wallet.is_singlesig and device.supports_qr_message_signing %}
|
||||
let redirectUrl = `{{ url_for('devices_endpoint.device', device_alias=device.alias, origin='wallet', address='ADDRESS', derivation_path='DERIVATION_PATH') }}`.replace('ADDRESS', address).replace('DERIVATION_PATH', derivation_path);
|
||||
addressInfoHTML += `<tr><td>{{ _("Message signing") }}:</td><td>
|
||||
<button type="button" id="msg-signing-btn" class="signing-btn" onclick="location.href='${redirectUrl}';">
|
||||
<button type="button" id="msg-signing-btn" class="btn" onclick="location.href='${redirectUrl}';">
|
||||
<div style="word-wrap: anywhere">🖌 {{ _("Sign a message with your") }} {{ device.name }} {{ _("device") }}</div>
|
||||
</button></td></tr>`;
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@
|
|||
data-label="${addressData.label}"
|
||||
data-address-wallet="${wallet}"
|
||||
/>`;
|
||||
showPageOverlay('address-popup');
|
||||
showPageOverlay('address-popup', null, 'start');
|
||||
} else {
|
||||
copyText(address, `{{ _("Copied address") }}: ${ address }`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<div class="hidden z-30 absolute w-screen h-screen" id="page_overlay">
|
||||
<div class="w-screen overflow-auto h-screen bg-black/40 flex flex-col relative justify-start items-center py-5" id="page_overlay_popup">
|
||||
<div class="w-screen overflow-auto h-screen bg-black/40 flex flex-col relative justify-center items-center py-5" id="page_overlay_popup">
|
||||
<div class="box-shadow-xl transition-all absolute max-w-[700px] overflow-hidden rounded-xl text-white" id="page_overlay_popup_content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
document.getElementById('pacman').style.display='flex';
|
||||
}
|
||||
|
||||
function showPageOverlay(container_id, position) {
|
||||
function showPageOverlay(container_id, position, location) {
|
||||
// Transfer the content to the popup overlay div
|
||||
document.getElementById("page_overlay_popup_content").appendChild(document.getElementById(container_id));
|
||||
|
||||
|
|
@ -47,6 +47,10 @@
|
|||
contentContainer.style.bottom = position.bottom || "auto";
|
||||
}
|
||||
|
||||
if (location) {
|
||||
overlayPopup.style.justifyContent = location
|
||||
}
|
||||
|
||||
// Show the content
|
||||
var pageContent = document.getElementById("page_overlay_popup_content").children[0];
|
||||
pageContent.style.display = "block";
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
|
||||
<form id="{{ device.alias }}_sign_tx_qr" class="flex-center flex-column hidden">
|
||||
<form id="{{ device.alias }}_sign_tx_qr" class="flex-center flex-column hidden p-4 bg-dark-800">
|
||||
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<h2>{{ _("Scan this QR code with your") }} {{device.name}}.</h2>
|
||||
|
||||
<div class="flex justify-between mb-2">
|
||||
<h3 class="mb-0">{{ _("Scan this QR code with your") }} {{device.name}}.</h3>
|
||||
<p class="m-0 cursor-pointer" onclick="hidePageOverlay()" class="cursor-pointer">Close</p>
|
||||
</div>
|
||||
|
||||
<div class="row" data-style="min-height: 400px; margin-top:20px">
|
||||
<span id="compressed_psbt" data-style="margin: auto;">
|
||||
<qr-code animate="{{device.qr_code_animate}}" frame-rate="{{device.qr_code_frame_rate}}" class='center' value="{{ device_psbts['qrcode'] }}" width="400" scalable></qr-code>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<qr-scanner id="{{ device.alias }}_qr_scan_btn">
|
||||
<a slot="button" class="btn flex-item" data-style="width: 250px; margin: 15px auto;">
|
||||
<a slot="button" class="button bg-accent">
|
||||
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" data-style="width: 30px; margin: 0px;" class="svg-white"> {{ _("Scan signed transaction") }}
|
||||
</a>
|
||||
</qr-scanner>
|
||||
|
|
@ -30,4 +35,4 @@
|
|||
}
|
||||
});
|
||||
</script>
|
||||
</form>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
|
||||
<form id="{{ device.alias }}_sign_msg_qr" class="flex-center flex-column hidden">
|
||||
<form id="{{ device.alias }}_sign_msg_qr" class="flex-center flex-column hidden p-4 bg-dark-800">
|
||||
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<h2>{{ _("Scan this QR code with your") }} {{device.name}}</h2>
|
||||
|
||||
<div class="flex justify-between mb-2 space-x-5">
|
||||
<h3 class="mb-0" data-cy="scan-qr-code-for-msg-signing-overlay-headline">{{ _("Scan this QR code with your") }} {{device.name}}</h3>
|
||||
<p class="m-0 ml-5 inline-block cursor-pointer" onclick="hidePageOverlay()" data-cy="scan-qr-code-for-msg-signing-overlay-close-btn">Close</p>
|
||||
</div>
|
||||
|
||||
<div class="row" data-style="min-height: 400px; margin-top:20px">
|
||||
<span id="compressed_msg" data-style="margin: auto;">
|
||||
<qr-code id="msg-qr-code" animate="auto" data-style="margin-top:20px;" width="300"></qr-code>
|
||||
</span>
|
||||
</div>
|
||||
<qr-scanner id="{{ device.alias }}_qr_scan_btn">
|
||||
<a slot="button" class="btn flex-item" data-style="width: 350px; margin: 15px auto; font-size: 1em;">
|
||||
📷 {{ _("Click here to scan signature") }}
|
||||
<a slot="button" class="button bg-accent mb-0 mt-3 w-full">
|
||||
{{ _("Click here to scan signature") }}
|
||||
</a>
|
||||
</qr-scanner>
|
||||
|
||||
|
|
@ -31,4 +35,4 @@
|
|||
}
|
||||
});
|
||||
</script>
|
||||
</form>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
<div class="mx-3 mt-3" id="services_list">
|
||||
<div class="mx-3" id="services_list">
|
||||
{% for _,plugin in specter.service_manager.services.items() %}
|
||||
{% if plugin.id in current_user.services and plugin.visible_in_sidebar %}
|
||||
<a class="flex items-center space-x-2" href="{{ url_for(plugin.id +'_endpoint.index') }}" class="item service">
|
||||
<img class="w-8 h-8" src="{{ url_for(plugin.id +'_endpoint' + '.static', filename=plugin.icon) }}">
|
||||
<div>{{ plugin.name }}</div>
|
||||
</a>
|
||||
<br>
|
||||
<a class="flex items-center my-1 h-12 space-x-2" href="{{ url_for(plugin.id +'_endpoint.index') }}" class="item service">
|
||||
<img class="w-8 h-8" src="{{ url_for(plugin.id +'_endpoint' + '.static', filename=plugin.icon) }}">
|
||||
<div>{{ plugin.name }}</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -65,17 +65,17 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="overflow-scroll p-3 flex flex-col grow">
|
||||
<div class="mx-3 flex items-center justify-between {% if not specter.chain %}opacity-40 pointer-events-none{% else %}cursor-pointer{% endif %}">
|
||||
<span class="" id="toggle_wallets_list">{{ _("Wallets") }} ▸</span>
|
||||
<img class="rounded-full bg-dark-700 w-4 h-4 animate-spin {% if not specter.wallet_manager.is_loading or not specter.chain %} hidden {% endif %}" id="wallets-loading-indicator" data-style="vertical-align: sub;" src="{{ url_for('static', filename='img/refresh.svg') }}"/>
|
||||
</div>
|
||||
|
||||
<div class="mx-3 mt-3 hidden" id="reload-wallets-list-btn">
|
||||
<a class="text-white flex items-center rounded-lg bg-dark-600 hover:bg-dark-700 flex items-center justify-center py-1" onclick=reloadPage()>
|
||||
<svg class="text-white w-5 h-5 mr-2" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><!--Generated by IJSVG (https://github.com/iconjar/IJSVG)--><path d="M52.8359,43.6067l1.30463e-08,-1.88843e-08c-0.312506,0.452349 -0.932543,0.565714 -1.38489,0.253208c-0.0990138,-0.0684038 -0.184804,-0.154194 -0.253208,-0.253208l-6.9888,-9.9742l-5.91546e-08,-8.31834e-08c-0.328807,-0.46237 -0.220533,-1.10375 0.241837,-1.43255c0.168889,-0.120103 0.370155,-0.186318 0.577363,-0.189948h4.9835l3.54852e-07,0.000503677c-0.00686545,-9.94223 -8.0722,-17.9964 -18.0144,-17.9896c-4.13508,0.00285541 -8.14312,1.42919 -11.3504,4.03924l4.60883e-08,-3.91242e-08c-0.842285,0.715013 -2.10472,0.611839 -2.81974,-0.230446c-0.715013,-0.842285 -0.611839,-2.10472 0.230446,-2.81974l3.46098e-07,-2.83814e-07c9.39634,-7.70537 23.26,-6.33457 30.9654,3.06177c3.22304,3.93035 4.98589,8.85536 4.98903,13.9382h4.9935l2.44822e-09,4.33431e-11c0.567328,0.0100438 1.0191,0.478096 1.00905,1.04542c-0.0036664,0.207098 -0.0698445,0.408252 -0.189853,0.577075Zm-40.0129,-23.1717l6.9888,9.9742l-2.71216e-09,-3.81325e-09c0.32881,0.4623 0.220594,1.10362 -0.241706,1.43243c-0.168809,0.120065 -0.369975,0.186285 -0.577094,0.189969h-4.983l2.60913e-07,0.000383721c0.0068661,9.94206 8.07207,17.9961 18.0141,17.9893c4.1354,-0.00285595 8.14372,-1.42943 11.3511,-4.03989l-7.47965e-08,6.34738e-08c0.8424,-0.714877 2.10482,-0.6115 2.8197,0.2309c0.714877,0.8424 0.6115,2.10482 -0.2309,2.8197l4.04125e-07,-3.31403e-07c-9.39617,7.70531 -23.2597,6.33461 -30.965,-3.06156c-3.22311,-3.93039 -4.98597,-8.85549 -4.98903,-13.9384h-4.9947l5.45126e-08,9.68797e-10c-0.567328,-0.0100825 -1.01906,-0.478166 -1.00898,-1.04549c0.00367938,-0.207033 0.0698308,-0.408123 0.189782,-0.576906l6.9888,-9.9742l-3.21808e-08,4.66031e-08c0.312407,-0.452417 0.93242,-0.565917 1.38484,-0.25351c0.0990287,0.0683822 0.184838,0.154154 0.253263,0.253153Z" fill="currentColor" fill-rule="evenodd"></path></svg>
|
||||
<div>Refresh list</div>
|
||||
</a>
|
||||
<div class="overflow-scroll font-medium p-3 flex flex-col grow">
|
||||
<div id="wallets_toggle" class="cursor-pointer text-dark-200 group py-1 px-3 font-medium hover:bg-dark-800 rounded-lg flex justify-between items-center hover:text-dark-50">
|
||||
<div class="flex items-center w-full justify-between {% if not specter.chain %}opacity-40 pointer-events-none{% endif %}">
|
||||
<div class="space-x-2 flex items-center">
|
||||
<svg id="wallets_caret" class="rotate-90 transition-all fill-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.6742 9.15634C15.2917 9.54929 15.2917 10.4507 14.6742 10.8437L6.53688 16.022C5.87115 16.4456 5 15.9674 5 15.1783L5 4.82167C5 4.03258 5.87115 3.55437 6.53688 3.97801L14.6742 9.15634Z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>{{ _("Wallets") }}</span>
|
||||
</div>
|
||||
<img class="rounded-full bg-dark-700 w-4 h-4 animate-spin {% if not specter.wallet_manager.is_loading or not specter.chain %} hidden {% endif %}" id="wallets-loading-indicator" data-style="vertical-align: sub;" src="{{ url_for('static', filename='img/refresh.svg') }}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-3" id="wallets_list">
|
||||
|
|
@ -117,6 +117,7 @@
|
|||
{% for wallet in specter.wallet_manager.failed_load_wallets %}
|
||||
<div class="bg-dark-800 p-4 rounded-lg ">
|
||||
<h1>{{ _("Wallet Name") }}: {{wallet.name}}</h1>
|
||||
<h5>{{ _("Wallet alias") }}: {{wallet.alias}}</h5>
|
||||
<h2>{{ _("Error Details") }}:</h2>
|
||||
<p data-style="color: red">{{wallet.loading_error}}</p>
|
||||
<form class="mt-3" action="{{ url_for('wallets_endpoint.failed_wallets') }}" method="POST" class="row center">
|
||||
|
|
@ -148,12 +149,24 @@
|
|||
<!-- {% endif %} -->
|
||||
</div>
|
||||
|
||||
<div class="mx-3 mt-3 {% if not specter.chain %}opacity-40 pointer-events-none{% endif %}">
|
||||
{{ sidebar_btn(url_for('wallets_endpoint.new_wallet_type'), _('Add new wallet'), 'btn_new_wallet', icon=false) }}
|
||||
<div class="mx-3 mt-3 cursor-pointer hidden" id="reload-wallets-list-btn">
|
||||
<a class="text-white flex items-center rounded-lg bg-dark-600 hover:bg-dark-700 flex items-center justify-center py-1" onclick=reloadPage()>
|
||||
<svg class="text-white w-5 h-5 mr-2" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><!--Generated by IJSVG (https://github.com/iconjar/IJSVG)--><path d="M52.8359,43.6067l1.30463e-08,-1.88843e-08c-0.312506,0.452349 -0.932543,0.565714 -1.38489,0.253208c-0.0990138,-0.0684038 -0.184804,-0.154194 -0.253208,-0.253208l-6.9888,-9.9742l-5.91546e-08,-8.31834e-08c-0.328807,-0.46237 -0.220533,-1.10375 0.241837,-1.43255c0.168889,-0.120103 0.370155,-0.186318 0.577363,-0.189948h4.9835l3.54852e-07,0.000503677c-0.00686545,-9.94223 -8.0722,-17.9964 -18.0144,-17.9896c-4.13508,0.00285541 -8.14312,1.42919 -11.3504,4.03924l4.60883e-08,-3.91242e-08c-0.842285,0.715013 -2.10472,0.611839 -2.81974,-0.230446c-0.715013,-0.842285 -0.611839,-2.10472 0.230446,-2.81974l3.46098e-07,-2.83814e-07c9.39634,-7.70537 23.26,-6.33457 30.9654,3.06177c3.22304,3.93035 4.98589,8.85536 4.98903,13.9382h4.9935l2.44822e-09,4.33431e-11c0.567328,0.0100438 1.0191,0.478096 1.00905,1.04542c-0.0036664,0.207098 -0.0698445,0.408252 -0.189853,0.577075Zm-40.0129,-23.1717l6.9888,9.9742l-2.71216e-09,-3.81325e-09c0.32881,0.4623 0.220594,1.10362 -0.241706,1.43243c-0.168809,0.120065 -0.369975,0.186285 -0.577094,0.189969h-4.983l2.60913e-07,0.000383721c0.0068661,9.94206 8.07207,17.9961 18.0141,17.9893c4.1354,-0.00285595 8.14372,-1.42943 11.3511,-4.03989l-7.47965e-08,6.34738e-08c0.8424,-0.714877 2.10482,-0.6115 2.8197,0.2309c0.714877,0.8424 0.6115,2.10482 -0.2309,2.8197l4.04125e-07,-3.31403e-07c-9.39617,7.70531 -23.2597,6.33461 -30.965,-3.06156c-3.22311,-3.93039 -4.98597,-8.85549 -4.98903,-13.9384h-4.9947l5.45126e-08,9.68797e-10c-0.567328,-0.0100825 -1.01906,-0.478166 -1.00898,-1.04549c0.00367938,-0.207033 0.0698308,-0.408123 0.189782,-0.576906l6.9888,-9.9742l-3.21808e-08,4.66031e-08c0.312407,-0.452417 0.93242,-0.565917 1.38484,-0.25351c0.0990287,0.0683822 0.184838,0.154154 0.253263,0.253153Z" fill="currentColor" fill-rule="evenodd"></path></svg>
|
||||
<div>Refresh wallets</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mx-3 mt-8 cursor-pointer">
|
||||
<span class="" id="toggle_devices_list" data-style="cursor: pointer;">{{ _("Devices") }} ▸</span>
|
||||
<div class="mx-3 mt-3 {% if not specter.chain %}opacity-40 pointer-events-none{% endif %}">
|
||||
{{ sidebar_btn(url_for('wallets_endpoint.new_wallet_type'), _('Add wallet'), 'btn_new_wallet', icon=false) }}
|
||||
</div>
|
||||
|
||||
<div id="devices_toggle" class="mt-8 cursor-pointer text-dark-200 group py-1 px-3 font-medium hover:bg-dark-800 rounded-lg flex justify-between items-center hover:text-dark-50">
|
||||
<div class="space-x-2 flex items-center">
|
||||
<svg id="devices_caret" class="rotate-90 transition-all fill-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.6742 9.15634C15.2917 9.54929 15.2917 10.4507 14.6742 10.8437L6.53688 16.022C5.87115 16.4456 5 15.9674 5 15.1783L5 4.82167C5 4.03258 5.87115 3.55437 6.53688 3.97801L14.6742 9.15634Z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>{{ _("Devices") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% from 'includes/sidebar/components/sidebar_device_list_item.jinja' import sidebar_device_list_item %}
|
||||
|
|
@ -164,17 +177,22 @@
|
|||
</div>
|
||||
|
||||
<div class="mx-3 mt-3">
|
||||
{{ sidebar_btn(url_for('devices_endpoint.new_device_type'), _('Add new device'),'btn_new_device', icon=false) }}
|
||||
{{ sidebar_btn(url_for('devices_endpoint.new_device_type'), _('Add device'),'btn_new_device', icon=false) }}
|
||||
</div>
|
||||
|
||||
<div class="mx-3 mt-8">
|
||||
<span class="" id="toggle_services_list" data-style="cursor: pointer;">Plugins</span>
|
||||
<div id="services_toggle" class="mt-8 cursor-pointer text-dark-200 group py-1 px-3 font-medium hover:bg-dark-800 rounded-lg flex justify-between items-center hover:text-dark-50">
|
||||
<div class="space-x-2 flex items-center">
|
||||
<svg id="services_caret" class="rotate-90 transition-all fill-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.6742 9.15634C15.2917 9.54929 15.2917 10.4507 14.6742 10.8437L6.53688 16.022C5.87115 16.4456 5 15.9674 5 15.1783L5 4.82167C5 4.03258 5.87115 3.55437 6.53688 3.97801L14.6742 9.15634Z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>{{ _("Services") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include "includes/sidebar/components/sidebar_services_list.jinja" %}
|
||||
|
||||
<div class="mx-3 mt-3">
|
||||
{{ sidebar_btn(url_for('services_endpoint.choose'), 'Choose plugins', 'btn_plugins', icon=false) }}
|
||||
{{ sidebar_btn(url_for('services_endpoint.choose'), 'Add plugin', 'btn_plugins', icon=false) }}
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
|
|
@ -238,20 +256,53 @@
|
|||
{% endif %}
|
||||
|
||||
<script>
|
||||
if (localStorage.wallets === 'true' || localStorage.wallets === undefined) {
|
||||
document.getElementById('wallets_list').classList.remove('hidden')
|
||||
document.getElementById('toggle_wallets_list').innerHTML = 'Wallets ▾';
|
||||
} else {
|
||||
document.getElementById('wallets_list').classList.add('hidden')
|
||||
document.getElementById('toggle_wallets_list').innerHTML = 'Wallets ▸';
|
||||
let wallets_toggle = document.getElementById('wallets_toggle');
|
||||
let wallets_caret = document.getElementById('wallets_caret');
|
||||
let wallets_list = document.getElementById('wallets_list');
|
||||
|
||||
let devices_toggle = document.getElementById('devices_toggle');
|
||||
let devices_caret = document.getElementById('devices_caret');
|
||||
let devices_list = document.getElementById('devices_list');
|
||||
|
||||
let services_toggle = document.getElementById('services_toggle');
|
||||
let services_caret = document.getElementById('services_caret');
|
||||
let services_list = document.getElementById('services_list');
|
||||
|
||||
if (localStorage.getItem('wallets_hidden') === 'true') {
|
||||
wallets_caret.classList.remove('rotate-90')
|
||||
wallets_list.classList.add('hidden')
|
||||
}
|
||||
|
||||
if (localStorage.devices === 'true' || localStorage.devices === undefined) {
|
||||
document.getElementById('devices_list').classList.remove('hidden')
|
||||
document.getElementById('toggle_devices_list').innerHTML = 'Devices ▾';
|
||||
} else {
|
||||
document.getElementById('devices_list').classList.add('hidden')
|
||||
document.getElementById('toggle_devices_list').innerHTML = 'Devices ▸';
|
||||
if (localStorage.getItem('devices_hidden') === 'true') {
|
||||
devices_caret.classList.remove('rotate-90')
|
||||
devices_list.classList.add('hidden')
|
||||
}
|
||||
|
||||
if (localStorage.getItem('services_hidden') === 'true') {
|
||||
services_caret.classList.remove('rotate-90')
|
||||
services_list.classList.add('hidden')
|
||||
}
|
||||
|
||||
function toggleList(type) {
|
||||
let toggle = document.getElementById(type + '_toggle');
|
||||
let caret = document.getElementById(type + '_caret');
|
||||
let list = document.getElementById(type + '_list');
|
||||
|
||||
if (list.classList.contains('hidden')) {
|
||||
// The list is hidden
|
||||
localStorage.removeItem(type + '_hidden')
|
||||
|
||||
// Make it visible
|
||||
list.classList.remove('hidden')
|
||||
caret.classList.add('rotate-90')
|
||||
} else {
|
||||
// The list is visible
|
||||
localStorage.setItem(type + '_hidden', true)
|
||||
|
||||
// Make it hidden
|
||||
list.classList.add('hidden')
|
||||
caret.classList.remove('rotate-90')
|
||||
}
|
||||
}
|
||||
|
||||
async function updateWalletsLoadingData() {
|
||||
|
|
@ -283,44 +334,25 @@
|
|||
location.reload()
|
||||
}
|
||||
|
||||
function toggleList(type, display_text) {
|
||||
let titleButton = document.getElementById('toggle_' + type + '_list');
|
||||
let list = document.getElementById(type + '_list');
|
||||
if (!list.classList || list.classList.contains('hidden')) {
|
||||
document.getElementById(type + '_list').classList.remove('hidden')
|
||||
localStorage[type] = true;
|
||||
titleButton.innerHTML = display_text + ' ▾';
|
||||
} else {
|
||||
document.getElementById(type + '_list').classList.add('hidden')
|
||||
localStorage[type] = false;
|
||||
titleButton.innerHTML = display_text + ' ▸';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function(){
|
||||
if ('{{ specter.device_manager.devices_names | length }}' == '0') {
|
||||
document.getElementById('toggle_devices_list').innerHTML = `{{ _("Devices") }}`;
|
||||
} else {
|
||||
document.getElementById('toggle_devices_list').addEventListener('click', (event) => {
|
||||
toggleList('devices', '{{ _("Devices") }}');
|
||||
});
|
||||
}
|
||||
/*
|
||||
* Set event listeners
|
||||
*/
|
||||
|
||||
if ('{{ specter.wallet_manager.wallets_names | length }}' == '0') {
|
||||
document.getElementById('toggle_wallets_list').innerHTML = `{{ _("Wallets") }}`;
|
||||
} else {
|
||||
document.getElementById('toggle_wallets_list').addEventListener('click', (event) => {
|
||||
toggleList('wallets', '{{ _("Wallets") }}');
|
||||
});
|
||||
}
|
||||
if ('{{ specter.wallet_manager.wallets_names | length }}' == '0') {}
|
||||
wallets_toggle.addEventListener('click', (event) => {
|
||||
toggleList('wallets');
|
||||
});
|
||||
|
||||
if ('{{ specter.service_manager.service_names | length }}' == '0') {
|
||||
document.getElementById('toggle_services_list').innerHTML = 'Plugins';
|
||||
} else {
|
||||
document.getElementById('toggle_services_list').addEventListener('click', (event) => {
|
||||
toggleList('services');
|
||||
});
|
||||
}
|
||||
if ('{{ specter.device_manager.devices_names | length }}' == '0') {}
|
||||
devices_toggle.addEventListener('click', (event) => {
|
||||
toggleList('devices');
|
||||
});
|
||||
|
||||
if ('{{ specter.service_manager.service_names | length }}' == '0') {}
|
||||
services_toggle.addEventListener('click', (event) => {
|
||||
toggleList('services');
|
||||
});
|
||||
|
||||
{% if specter.wallet_manager.is_loading %}
|
||||
updateWalletsLoadingData();
|
||||
|
|
|
|||
|
|
@ -1176,7 +1176,7 @@
|
|||
data-txid="${txid}"
|
||||
data-tx-wallet="${wallet}">
|
||||
</tx-data>`;
|
||||
showPageOverlay('tx-popup');
|
||||
showPageOverlay('tx-popup', null, 'start');
|
||||
} else {
|
||||
copyText(txid, `{{ _("Copied transaction") }}: ${ txid }`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,22 +9,24 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% if specter.chain %}
|
||||
<table>
|
||||
<tr> <td data-style="text-align: left;">{{ _("Network") }}:</td> <td data-style="text-align: right;" id="node-info-specter-chain">{{specter.chain}}</td> </tr>
|
||||
<tr> <td data-style="text-align: left;">{{ _("Blocks count") }}:</td> <td data-style="text-align: right;">{{specter.info['blocks']}}</td> </tr>
|
||||
<tr> <td data-style="text-align: left;">{{ _("Host") }}:</td> <td data-style="text-align: right;" id="node-info-specter-chain">{{specter.node.host}}</td> </tr>
|
||||
<tr> <td data-style="text-align: left;">{{ _("Port") }}:</td> <td data-style="text-align: right;" id="node-info-specter-chain">{{specter.node.port}}</td> </tr>
|
||||
<tr> <td data-style="text-align: left; width: 35%;">{{ _("Bitcoin Core Version") }}:</td> <td data-style="text-align: right;">v{{specter.bitcoin_core_version}}</td> </tr>
|
||||
<tr> <td data-style="text-align: left;">{{ _("Connections count") }}:</td> <td data-style="text-align: right;">{{specter.network_info['connections']}}</td> </tr>
|
||||
<tr> <td data-style="text-align: left;">{{ _("Size on disk") }}:</td> <td data-style="text-align: right;">{{specter.info['size_on_disk']|bytessize }}</td> </tr>
|
||||
<tr> <td data-style="text-align: left;">{{ _("Mempool size") }}:</td> <td data-style="text-align: right;">{{specter.info['mempool_info'].size}} transactions</td> </tr>
|
||||
<tr> <td data-style="text-align: left;">{{ _("Node uptime") }}:</td> <td data-style="text-align: right;">~ {{(specter.info['uptime'] / 60 // 60) | int }} {{ _("Hours") }}</td> </tr>
|
||||
{% if specter.info['pruned'] %}
|
||||
<tr> <td data-style="text-align: left;">{{ _("Automatic pruning") }}:</td> <td data-style="text-align: right;">{{specter.info['automatic_pruning']}}</td> </tr>
|
||||
<tr> <td data-style="text-align: left;">{{ _("Prune height") }}:</td> <td data-style="text-align: right;">{{specter.info['pruneheight']}}</td> </tr>
|
||||
<tr> <td data-style="text-align: left;">{{ _("Prune target size") }}:</td> <td data-style="text-align: right;">{{specter.info['prune_target_size']}}</td> </tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
<div class="m-3 mb-1">
|
||||
<table>
|
||||
<tr> <td>{{ _("Network") }}</td> <td class="text-right" id="node-info-specter-chain">{{specter.chain}}</td> </tr>
|
||||
<tr> <td>{{ _("Blocks count") }}</td> <td class="text-right">{{specter.info['blocks']}}</td> </tr>
|
||||
<tr> <td>{{ _("Host") }}</td> <td class="text-right" id="node-info-specter-chain">{{specter.node.host}}</td> </tr>
|
||||
<tr> <td>{{ _("Port") }}</td> <td class="text-right" id="node-info-specter-chain">{{specter.node.port}}</td> </tr>
|
||||
<tr> <td>{{ _("Bitcoin Core Version") }}</td> <td class="text-right">v{{specter.bitcoin_core_version}}</td> </tr>
|
||||
<tr> <td>{{ _("Connections count") }}</td> <td class="text-right">{{specter.network_info['connections']}}</td> </tr>
|
||||
<tr> <td>{{ _("Size on disk") }}</td> <td class="text-right">{{specter.info['size_on_disk']|bytessize }}</td> </tr>
|
||||
<tr> <td>{{ _("Mempool size") }}</td> <td class="text-right">{{specter.info['mempool_info'].size}} transactions</td> </tr>
|
||||
<tr> <td>{{ _("Node uptime") }}</td> <td class="text-right">~ {{(specter.info['uptime'] / 60 // 60) | int }} {{ _("Hours") }}</td> </tr>
|
||||
{% if specter.info['pruned'] %}
|
||||
<tr> <td>{{ _("Automatic pruning") }}</td> <td class="text-right">{{specter.info['automatic_pruning']}}</td> </tr>
|
||||
<tr> <td>{{ _("Prune height") }}</td> <td class="text-right">{{specter.info['pruneheight']}}</td> </tr>
|
||||
<tr> <td>{{ _("Prune target size") }}</td> <td class="text-right">{{specter.info['prune_target_size']}}</td> </tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
{% if specter.bitcoin_core_version_raw < 200000 %}
|
||||
<div class="flex p-4 m-3 mb-4 bg-dark-600 rounded-lg" role="alert">
|
||||
<svg class="flex-shrink-0 inline w-6 h-6 mr-3" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><!--Generated by IJSVG (https://github.com/iconjar/IJSVG)--><path d="M32.0022,55.9844l-1.04907e-06,-2.84217e-14c-13.2548,-5.79387e-07 -24,-10.7452 -24,-24c5.79387e-07,-13.2548 10.7452,-24 24,-24c13.2548,5.79387e-07 24,10.7452 24,24l2.13163e-14,-1.04907e-06c0,13.2548 -10.7452,24 -24,24Zm1.5,-32l3.82016e-08,-2.71086e-09c-1.79133,0.127116 -3.35165,-1.21031 -3.5,-3l6.50646e-08,-7.8524e-07c0.148296,-1.78972 1.70866,-3.12717 3.5,-3l-5.85322e-09,4.15529e-10c1.79134,-0.127171 3.3517,1.21028 3.5,3l-5.57104e-08,6.72365e-07c-0.148295,1.78976 -1.70872,3.12723 -3.5001,3Zm1.4206,6.7635l-2.7853,10.5093l-8.37179e-08,3.15667e-07c-0.270677,1.02062 0.262417,2.08281 1.2425,2.4757l-5.91015e-08,-2.36708e-08c0.379505,0.151996 0.628277,0.519688 0.6282,0.9285v0.323l8.52651e-14,4.05e-07c0,0.551933 -0.447167,0.999503 -0.9991,1h-2.9891l-4.04477e-08,1.96948e-10c-1.65938,0.00807986 -3.01111,-1.33056 -3.01919,-2.98993c-0.00127082,-0.260991 0.0314695,-0.521035 0.0973943,-0.773566l2.7852,-10.5094l2.79671e-08,-1.05468e-07c0.270652,-1.02067 -0.262537,-2.08289 -1.2427,-2.4757l-2.20542e-08,-8.83296e-09c-0.379505,-0.151996 -0.628276,-0.519688 -0.6282,-0.9285v-0.3229l-1.77636e-14,-9.40042e-08c-8.33514e-08,-0.552011 0.447289,-0.999614 0.9993,-1h2.9887l1.06497e-07,-5.28143e-10c1.65943,-0.00822943 3.01133,1.33033 3.01956,2.98976c0.00129458,0.261047 -0.0314356,0.52115 -0.0973639,0.773738Z" fill="currentColor" fill-rule="evenodd"></path></svg>
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@
|
|||
|
||||
<div class="grid grid-cols-2 gap-3 mb-3">
|
||||
<div class="floating-wrapper">
|
||||
<input class="floating-input peer" type="text" id="host" name="host" type="text" {% if node.host %} value="{{ node.protocol }}://{{ node.host }}" {% else %} placeholder="" {% endif %}>
|
||||
<input class="floating-input peer" type="text" id="host" name="host" type="text" {% if node.host %} value="{{ node.protocol }}://{{ node.host }}" {% else %} placeholder=" " {% endif %}>
|
||||
<label class="floating-label" for="host">Host</label>
|
||||
</div>
|
||||
<div class="floating-wrapper">
|
||||
|
|
|
|||
|
|
@ -150,6 +150,15 @@
|
|||
</div>
|
||||
</text-explainer>
|
||||
|
||||
<text-explainer class="p-3">
|
||||
<span slot="title" class="cursor-pointer">{{ _("Support Tools") }} </span>
|
||||
<div slot="content" class="mt-3">
|
||||
<a class="btn" href="{{url_for('spectrum_endpoint.wallets_get') }}">
|
||||
Wallet Management
|
||||
</a>
|
||||
</div>
|
||||
</text-explainer>
|
||||
|
||||
<div class="mt-8">
|
||||
<button type="submit" class="button bg-accent text-white" name="action" value="save">{{ _("Save") }}</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<h3 class="mt-8">Information</h3>
|
||||
<div class="floating-wrapper">
|
||||
<input type="text" pattern="^[^']+" title="Do not use single quotes" id="wallet_name" name="wallet_name" class="peer floating-input" value="{{ wallet_name }}" placeholder=" " required>
|
||||
<input type="text" pattern="^[^']+" title="Choose a unique name. Capitalization, spaces, and hyphens will not affect uniqueness. Don't use single quotes." id="wallet_name" name="wallet_name" class="peer floating-input" value="{{ wallet_name }}" placeholder=" " required>
|
||||
<label class="floating-label">Wallet Name</label>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{{ _("Specter can verify this address if you scan it.")}}<br>{{ _("It has an address index included in the QR code.") }}
|
||||
</p><br>
|
||||
</div>
|
||||
<button onclick="verifyQRCode('{{ wallet.address }}', '{{ wallet.address_index | string }}')" type="button" class="btn centered padded">{{ _("Verify address via QR code") }}</button>
|
||||
<button onclick="verifyQRCode('{{ wallet.address }}', '{{ wallet.address_index | string }}')" type="button" class="button">{{ _("Verify address via QR code") }}</button>
|
||||
{% endif %}
|
||||
{% set supports_hwi = [] %}
|
||||
{% set supports_hwi_multisig_display_address = [] %}
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@
|
|||
|
||||
<h3>{{ _("Export to wallet software") }}</h3>
|
||||
<p>{{ _("Export this wallet to another Specter instance or other supported wallet softwares by scanning the QR code or importing the JSON file.") }}</p>
|
||||
<button onclick="showPageOverlay('account_map_export')" type="button" class="button mt-3" data-cy="show-export-details-overlay-btn">{{ _("Go to export details") }}</button>
|
||||
<button onclick="showPageOverlay('account_map_export', null, 'start')" type="button" class="button mt-3" data-cy="show-export-details-overlay-btn">{{ _("Go to export details") }}</button>
|
||||
|
||||
<h3 class="mt-8">{{ _("Specter wallet backup") }}</h3>
|
||||
<p>{{ _("Create a JSON file containing all wallet data including labels.") }}</p>
|
||||
|
|
|
|||
|
|
@ -8,11 +8,8 @@
|
|||
{% endif %}
|
||||
|
||||
<div class="max-w-prose">
|
||||
<p>{{ _("Specter is a convenient wallet to self-custody your bitcoin. It’s great for single signature wallets, but is specifically designed with a focus on multiple signature setups with multiple signing devices.") }}</p>
|
||||
<p class="mt-6">
|
||||
{{ _("Learn more about ") }}
|
||||
<a href="https://specter-desktop-docs.netlify.app/multisig-guide/" target="_blank">{{ _("multiple signature setups here") }}</a>.
|
||||
</p>
|
||||
<p>{{ _("Specter empowers individuals, communities and businesses to control their own Bitcoin keys, improving their security and enabling them to manage their bitcoin with greater control.") }}</p>
|
||||
<!-- <p>{{ _("Let’s get you setup. First, let's decide whether you'll be using your own Bitcoin server, or whether you'd like to use one run by a trusted Bitcoin company.") }}</p> -->
|
||||
</div>
|
||||
|
||||
{% if not specter.chain %}
|
||||
|
|
|
|||
|
|
@ -166,3 +166,27 @@ def index_post():
|
|||
)
|
||||
|
||||
raise Exception(f"Unknown action: {action}")
|
||||
|
||||
|
||||
@spectrum_endpoint.route("/wallets", methods=["GET"])
|
||||
@login_required
|
||||
def wallets_get():
|
||||
wallets = ext().spectrum_node.get_rpc().listwallets()
|
||||
wallets_dict = {}
|
||||
for wallet_name in wallets:
|
||||
rpc = ext().spectrum_node.get_rpc().wallet(wallet_name)
|
||||
wallets_dict[wallet_name] = rpc.getwalletinfo()
|
||||
return render_template(
|
||||
"spectrum/spectrum_wallets.jinja", wallets=wallets, wallets_dict=wallets_dict
|
||||
)
|
||||
|
||||
|
||||
@spectrum_endpoint.route("/wallets", methods=["POST"])
|
||||
@login_required
|
||||
def wallets_post():
|
||||
wallet_name = request.form["wallet_name"]
|
||||
# Does not work yet
|
||||
# ext().spectrum_node.get_rpc().wallet()delete_wallet()
|
||||
logger.info("Would delete wallet if it would yet be possible!")
|
||||
flash("Deletion not yet implemented!")
|
||||
return redirect(url_for(f"{ SpectrumService.get_blueprint_name()}.wallets_get"))
|
||||
|
|
|
|||
|
|
@ -8,14 +8,16 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% if specter.chain %}
|
||||
<table>
|
||||
<tr> <td data-style="text-align: left;">{{ _("Network") }}:</td> <td data-style="text-align: right;" id="node-info-specter-chain">{{specter.chain}}</td> </tr>
|
||||
<tr> <td data-style="text-align: left;">{{ _("Blocks count") }}:</td> <td data-style="text-align: right;">{{specter.info['blocks']}}</td> </tr>
|
||||
<tr> <td data-style="text-align: left;">{{ _("Host") }}:</td> <td data-style="text-align: right;" id="node-info-specter-chain">{{specter.node.host}}</td> </tr>
|
||||
<tr> <td data-style="text-align: left;">{{ _("Port") }}:</td> <td data-style="text-align: right;" id="node-info-specter-chain">{{specter.node.port}}</td> </tr>
|
||||
<tr> <td data-style="text-align: left;">{{ _("SSL") }}:</td> <td data-style="text-align: right;" id="node-info-specter-chain">{{specter.node.ssl}}</td> </tr>
|
||||
<tr> <td data-style="text-align: left;">{{ _("Node uptime") }}:</td> <td data-style="text-align: right;">~ {{(specter.info['uptime'] / 60 // 60) | int }} {{ _("Hours") }}</td> </tr>
|
||||
</table>
|
||||
<div class="m-3 mb-1">
|
||||
<table>
|
||||
<tr> <td>{{ _("Network") }}</td> <td class="text-right" id="node-info-specter-chain">{{specter.chain}}</td> </tr>
|
||||
<tr> <td>{{ _("Blocks count") }}</td> <td class="text-right">{{specter.info['blocks']}}</td> </tr>
|
||||
<tr> <td>{{ _("Host") }}</td> <td class="text-right" id="node-info-specter-chain">{{specter.node.host}}</td> </tr>
|
||||
<tr> <td>{{ _("Port") }}</td> <td class="text-right" id="node-info-specter-chain">{{specter.node.port}}</td> </tr>
|
||||
<tr> <td>{{ _("SSL") }}</td> <td class="text-right" id="node-info-specter-chain">{{specter.node.ssl}}</td> </tr>
|
||||
<tr> <td>{{ _("Node uptime") }}</td> <td class="text-right">~ {{(specter.info['uptime'] / 60 // 60) | int }} {{ _("Hours") }}</td> </tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="px-3 pb-4 mt-2">
|
||||
{% if current_user.is_admin %}
|
||||
|
|
@ -57,4 +59,4 @@
|
|||
setTimeout(updateProgressBar, 10000);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -6,11 +6,10 @@
|
|||
- active_menuitem: Current active tab. Options: 'general', 'settings', ...
|
||||
#}
|
||||
{% macro spectrum_menu(active_menuitem) -%}
|
||||
<nav class="row collapse-on-mobile">
|
||||
{{ menu_item(service.id, 'index', 'Main', active_menuitem, isLeft=true) }}
|
||||
{{ menu_item(service.id, 'settings_get', 'Settings', active_menuitem, isRight=true) }}
|
||||
<a href="javascript:void(0);" class="mobile-nav-icon" onclick="toggleMobileNav(this, `{{ url_for('static', filename='img/expand-more.svg') }}`, `{{ url_for('static', filename='img/expand-less.svg') }}`)">
|
||||
<img style="width: 36px;" src="{{ url_for('static', filename='img/expand-more.svg') }}"/>
|
||||
</a>
|
||||
<nav class="text-center text-dark-200 border-b border-dark-600">
|
||||
<ul class="flex flex-wrap -mb-px">
|
||||
{{ menu_item(service.id, 'index', 'Main', active_menuitem) }}
|
||||
{{ menu_item(service.id, 'wallets_get', 'Wallet Management', active_menuitem, isRight=true) }}
|
||||
</ul>
|
||||
</nav>
|
||||
{%- endmacro %}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "spectrum/base.jinja" %}
|
||||
{% extends "spectrum/components/spectrum_tab.jinja" %}
|
||||
|
||||
{% block main %}
|
||||
<h1>{{ _("Connect with Electrum") }}</h1>
|
||||
|
|
@ -44,7 +44,6 @@
|
|||
</div>
|
||||
<div id ="elec_list_container" class="floating-wrapper">
|
||||
<select id="server-list" name="elec_option" class="floating-input peer">
|
||||
<option selected style='display: none'></option>
|
||||
{% for electrum_option in elec_options %}
|
||||
<option value="{{ electrum_option }}" {% if elec_chosen_option == electrum_option %}selected{% endif %}>{{ electrum_option }}</option>
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{% block main %}
|
||||
<div>
|
||||
<h1>{{ _("Spectrum - Connecting Specter to Electrum") }}</h1><br>
|
||||
<h1>{{ _("Connect with Electrum") }}</h1>
|
||||
<div>
|
||||
{% if host_type == "list" %}
|
||||
{% if success %}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
{% extends "spectrum/components/spectrum_tab.jinja" %}
|
||||
{% block title %}{% endblock %}
|
||||
{% set tab = 'index' %}
|
||||
{% block content %}
|
||||
<div>
|
||||
<h1>{{ _("Wallet Management") }}</h1>
|
||||
<div class="note">
|
||||
{{ _("Other than core, Spectrum lets you delete wallets. The limitation of core has been") }}<br/>
|
||||
{{ _("surprising effects about wallet naming. This could have lead to inconsitencies which are") }}<br/>
|
||||
{{ _("difficult to solve.") }}<br/>
|
||||
{{ _("Here, you can delete wallets if you really need to. Use it with care!") }}<br/>
|
||||
</div>
|
||||
<div class="table-holder">
|
||||
<table class="space-y-3 max-w-[700px] m-auto min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
||||
<th scope="col" class="py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Version</th>
|
||||
<th scope="col" class="py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Balance</th>
|
||||
<th scope="col" class="py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Un. Bl.</th>
|
||||
<th scope="col" class="py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Imm. Bl.</th>
|
||||
<th scope="col" class="py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tx #</th>
|
||||
<th scope="col" class="py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Keyp</th>
|
||||
<th scope="col" class="py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Keyp Int</th>
|
||||
<th scope="col" class="py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Pay Tx Fee</th>
|
||||
<th scope="col" class="py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-gray-200">
|
||||
{% for wallet_name, wallet in wallets_dict.items() %}
|
||||
<tr>
|
||||
<td class="py-4 ">{{ wallet['walletname'] }}</td>
|
||||
<td class="py-4 ">{{ wallet['walletversion'] }}</td>
|
||||
<td class="py-4 ">{{ wallet['balance'] }}</td>
|
||||
<td class="py-4 ">{{ wallet['uconfirmed_balance'] }}</td>
|
||||
<td class="py-4 ">{{ wallet['immature_balance'] }}</td>
|
||||
<td class="py-4 ">{{ wallet['txcount'] }}</td>
|
||||
<td class="py-4 ">{{ wallet['keypoolsize'] }}</td>
|
||||
<td class="py-4 ">{{ wallet['keypoolsize_hd_internal'] }}</td>
|
||||
<td class="py-4 ">{{ wallet['paytxfee'] }}</td>
|
||||
<td class="py-4 ">
|
||||
<form action="{{ url_for('spectrum_endpoint.wallets_post')}}" method="POST">
|
||||
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="wallet_name" value="{{ wallet['walletname'] }}"/>
|
||||
<button type="submit" name="action" value="delete" class="button">
|
||||
<img src="/static/img/cross_thick.svg" class="w-5">
|
||||
<tool-tip id="tooltip-wallet-deletion">
|
||||
<h4 slot="title">{{ _("Wallet deletion") }}</h4>
|
||||
<span slot="paragraph">
|
||||
{{ _("Clicking here would delete the Wallet but only on the Spectrum, not in Specter.") }}
|
||||
</span>
|
||||
</tool-tip>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import logging
|
||||
from cryptoadvance.specter.helpers import deep_update, load_jsons, is_ip_private, alias
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def test_deep_update():
|
||||
import cryptoadvance.specter.helpers as helpers
|
||||
|
||||
base_value = {
|
||||
"pli": {"pla": "blub", "yes": "yes"},
|
||||
"pli2": ["arrayelm1", "arrayelm2", "arrayelm3"],
|
||||
|
|
@ -18,7 +19,7 @@ def test_deep_update():
|
|||
"pli": {"newSubKey": "blub2"},
|
||||
"pli2": ["arrayelm4", "arrayelm5"],
|
||||
}
|
||||
helpers.deep_update(base_value, update_value)
|
||||
deep_update(base_value, update_value)
|
||||
# There is now a newRootKey
|
||||
assert len(base_value) == 4
|
||||
# keys get added
|
||||
|
|
@ -26,19 +27,18 @@ def test_deep_update():
|
|||
# Arrays get replaced, not appended!
|
||||
assert len(base_value["pli2"]) == 2
|
||||
# you cannot delete stuff with empty dicts
|
||||
helpers.deep_update(base_value, {"newRootKey": {}})
|
||||
deep_update(base_value, {"newRootKey": {}})
|
||||
assert base_value["newRootKey"]["pla"] == "blub"
|
||||
|
||||
|
||||
def test_load_jsons(caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
caplog.set_level(logging.DEBUG, logger="cryptoadvance.specter")
|
||||
import cryptoadvance.specter.helpers as helpers
|
||||
|
||||
mydict = helpers.load_jsons("./tests/helpers_testdata")
|
||||
mydict = 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 = 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"
|
||||
|
|
@ -56,7 +56,6 @@ def test_load_jsons(caplog):
|
|||
def test_is_ip_private(caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
caplog.set_level(logging.DEBUG, logger="cryptoadvance.specter")
|
||||
import cryptoadvance.specter.helpers as helpers
|
||||
|
||||
# https://en.wikipedia.org/wiki/Private_network
|
||||
# For each private network address range, we are testing the lower, arbitrary middle and upper part of the range
|
||||
|
|
@ -68,14 +67,43 @@ def test_is_ip_private(caplog):
|
|||
# Randomly generated public ip addresses
|
||||
public_addresses = ["58.28.76.138", "128.5.218.201", "170.45.214.32"]
|
||||
|
||||
assert helpers.is_ip_private("localhost")
|
||||
assert is_ip_private("localhost")
|
||||
for ip in priv_lo_addresses:
|
||||
assert helpers.is_ip_private(ip)
|
||||
assert is_ip_private(ip)
|
||||
for ip in priv_24_addresses:
|
||||
assert helpers.is_ip_private(ip)
|
||||
assert is_ip_private(ip)
|
||||
for ip in priv_20_addresses:
|
||||
assert helpers.is_ip_private(ip)
|
||||
assert is_ip_private(ip)
|
||||
for ip in priv_16_addresses:
|
||||
assert helpers.is_ip_private(ip)
|
||||
assert is_ip_private(ip)
|
||||
for ip in public_addresses:
|
||||
assert not helpers.is_ip_private(ip)
|
||||
assert not is_ip_private(ip)
|
||||
|
||||
|
||||
def test_alias():
|
||||
the_same_unique_names = [
|
||||
"ghost wallet",
|
||||
"Ghost Wallet",
|
||||
"GHOST WALLET",
|
||||
"gHoSt wALlEt",
|
||||
" Ghost-Wallet",
|
||||
"Ghost-Wallet ",
|
||||
" Ghost-Wallet ",
|
||||
"ghost-wallet",
|
||||
"Ghost_Wallet",
|
||||
"ghost_wallet",
|
||||
"ghost wallet",
|
||||
"ghost----wallet",
|
||||
"Ghost Wallet?",
|
||||
"Ghost Wallet***",
|
||||
]
|
||||
assert all(alias(name) == "ghost_wallet" for name in the_same_unique_names)
|
||||
not_the_same_unique_names = [
|
||||
"ghost wallet",
|
||||
"ghost wallet 2",
|
||||
"ghostwallet",
|
||||
"ghost.wallet",
|
||||
"ghostwallet123",
|
||||
"my_ghos_wallet",
|
||||
]
|
||||
assert not all(alias(name) == "ghost_wallet" for name in not_the_same_unique_names)
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ def test_device_wallets(
|
|||
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",
|
||||
"multisig_wallet_for_device_test",
|
||||
1,
|
||||
"wsh",
|
||||
[device.keys[7], second_device.keys[0]],
|
||||
|
|
|
|||
|
|
@ -42,9 +42,11 @@ def test_WalletManager(
|
|||
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(
|
||||
"wallet_for_wallet_manager_test", 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["wallet_for_wallet_manager_test"]
|
||||
assert wallet != None
|
||||
assert wallet.balance["trusted"] == 0
|
||||
assert wallet.balance["untrusted_pending"] == 0
|
||||
|
|
@ -76,12 +78,18 @@ def test_WalletManager(
|
|||
multisig_wallet.update_balance()
|
||||
assert multisig_wallet.amount_total == 4
|
||||
# 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",
|
||||
"wallet_for_wallet_manager_test",
|
||||
]
|
||||
|
||||
# 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"]
|
||||
assert wm.wallets_names == [
|
||||
"new_name_test_wallet",
|
||||
"wallet_for_wallet_manager_test",
|
||||
]
|
||||
|
||||
# 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
|
||||
|
|
@ -100,7 +108,7 @@ def test_WalletManager(
|
|||
):
|
||||
wm.delete_wallet(wallet, node)
|
||||
# Check that the wallet wasn't deleted in Specter because of the RpcError
|
||||
assert wm.wallets_names == ["a_test_wallet"]
|
||||
assert wm.wallets_names == ["wallet_for_wallet_manager_test"]
|
||||
# The following deletion should not remove the wallet file on the node
|
||||
assert node_with_empty_datadir.datadir == ""
|
||||
wm.rpc.loadwallet(wallet_rpc_path) # we need to load the wallet again
|
||||
|
|
@ -109,7 +117,8 @@ def test_WalletManager(
|
|||
# The wallet in Specter was already deleted, so trying to delete it again should raise a SpecterError
|
||||
wm.rpc.loadwallet(wallet_rpc_path) # we need to load the wallet again
|
||||
with pytest.raises(
|
||||
SpecterError, match="The wallet a_test_wallet has already been deleted."
|
||||
SpecterError,
|
||||
match="The wallet wallet_for_wallet_manager_test has already been deleted.",
|
||||
):
|
||||
assert wm.delete_wallet(wallet)
|
||||
|
||||
|
|
@ -137,9 +146,9 @@ def test_WalletManager_2_nodes(
|
|||
device = device_manager.get_by_alias("trezor")
|
||||
assert device != None
|
||||
first_wallet = wm.create_wallet(
|
||||
"a_test_wallet", 1, "wpkh", [device.keys[5]], [device]
|
||||
"wallet_for_test_with_two_nodes", 1, "wpkh", [device.keys[5]], [device]
|
||||
)
|
||||
assert wm.wallets_names == ["a_test_wallet"]
|
||||
assert wm.wallets_names == ["wallet_for_test_with_two_nodes"]
|
||||
assert wm.chain == "regtest"
|
||||
assert wm.working_folder.endswith("regtest")
|
||||
assert wm.rpc.port == 18543
|
||||
|
|
@ -154,7 +163,7 @@ def test_WalletManager_2_nodes(
|
|||
"regtest",
|
||||
] # wm.rpcs looks like this: {'regtest': <BitcoinRpc http://localhost:18543>, 'regtest2': <BitcoinRpc http://localhost:18544>}
|
||||
assert wm.rpc.port == 18544
|
||||
assert wm.wallets_names == ["a_test_wallet"]
|
||||
assert wm.wallets_names == ["wallet_for_test_with_two_nodes"]
|
||||
assert wm.chain == "regtest"
|
||||
assert wm.working_folder.endswith("test")
|
||||
second_wallet = wm.create_wallet(
|
||||
|
|
@ -162,7 +171,10 @@ def test_WalletManager_2_nodes(
|
|||
)
|
||||
# Note: "regtest2" is recognised by the get_network() from embit as Liquid, that is why there is an error in the logs saying the Bitcoin address is not valid since a Liquid address is derived.
|
||||
assert len(wm.wallets_names) == 2
|
||||
assert wm.wallets_names == ["a_regtest2_test_wallet", "a_test_wallet"]
|
||||
assert wm.wallets_names == [
|
||||
"a_regtest2_test_wallet",
|
||||
"wallet_for_test_with_two_nodes",
|
||||
]
|
||||
|
||||
|
||||
def test_WalletManager_check_duplicate_keys(empty_data_folder):
|
||||
|
|
@ -243,7 +255,7 @@ def test_wallet_sortedmulti(
|
|||
for i in range(2):
|
||||
if i == 0:
|
||||
multisig_wallet = wm.create_wallet(
|
||||
"a_multisig_test_wallet",
|
||||
"multisig_wallet_for_sortedmulti_test",
|
||||
1,
|
||||
"wsh",
|
||||
[device.keys[7], second_device.keys[0]],
|
||||
|
|
@ -251,7 +263,7 @@ def test_wallet_sortedmulti(
|
|||
)
|
||||
else:
|
||||
multisig_wallet = wm.create_wallet(
|
||||
"a_multisig_test_wallet",
|
||||
"another_multisig_wallet_for_sortedmulti_test",
|
||||
1,
|
||||
"wsh",
|
||||
[second_device.keys[0], device.keys[7]],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue