Babel integration (#1247)

* Default bitcoind timeout to 60s for all platforms

* Create bitcoind_setup_tasks.py

* Missing prop setter bugfix

Fixes #1221

* Early progress check-in

* Docs update, initial FR translation file

* Lots more text wrapping, babel.cfg fix for html

* docs update

* Updated translator procedures, added initial German, Hebrew, and Portugese files

* Update README.md

* Update README.md

* Update messages.po

Started the translation process using Poedit. Lines

* Test first modif in ES

First modification (only adding one line here) using Poedit and following the translation steps given by kdmukai.

If everything is working well here I am ready to start translating.

* Update messages.po

* Bugfix; multi-language mnemonic support

* Explicit language selection

* Translation support for strings with variable insertions

* Bugfixes; define supported langages in only one place

* Cleanup

* Update messages.po

* Update messages.po

* Latest translations integrated

* Regenerating messages; disabling fuzzy matching

* Compiled messages

* template bugfixes

* Use jinja's "tojson" to safely escape account_map device labels

* Update wallet_pdf.jinja

* Update helpers.py

* Update node.py

* Bugfixes

* Updated requirements.txt; bugfix on language selection on Settings

* Update wallets.py

* Require `mnemonic==0.19` for multi-language BIP39 wordlists

* Update iw translation

* "he" language code fix for Hebrew

* Update README.md

* Adding blank Greek file; minor README update

* Update messages.po

50% on this ... My ass is flat.

* Spanish translation file updated

* Update messages.po

* Extracted from the latest code; recompiled with the latest translations

* Added missing wrapped strings

* Update messages.po

* Updated Spanish translations

* Adding blank Dutch ("nl") translation file

* Blank Korean ("ko") translation file

* Blank Portugese ("pt") file updated

* Blank Turkish ("tr") translation file

* Adding blank Serbian ("sr") translation file

* Update messages.po

* Compiled latest Spanish translations

* Translation progress

Translated the first 181 strings

* Blank Italian ("it") translation file

* More translation work done

6667810 strings translated. Nice number, guess I'll stop here :)

* Compiled first batch of German translations

* Update messages.po

* Translation 100%

All done. Would be extra pleased if my name popped up somewhere in credits. Not required though :)

* Update messages.po

* Update messages.po

All done !!! :)

* Compiled in initial Greek translations

* Updated completion of German translation

* Initial Dutch translations compiled in

* Compiled full Spanish translations

* Re-ran extraction, update, compile

* Update Dutch translation

* Blank Icelandic ("is") translation file

* Blank Chinese (zh_Hans_CN and zh_hant_TW) translation files

* Compiled latest Dutch translations

* Add missing strings to translation files

* Blank Swedish (sv) translation file

* Blank Polish ("pl") translation file

* Blank Bulgarian ("bg") translation file

* Blank Russian ("ru") translation file

* Swedish 30% done

* Update messages.po

* Blank Croatian ("hr") translation file

* Delete messages.mo

* Added translator

Also fixed some warnings in poedit

* Babel docs update

* Compiled latest Dutch and Swedish translations

* Update messages.po

Completed 100%

* Update wallets.py

* Compiled in latest German translations

* Update messages.po

* Extracted, updated, compiled translation files

* Compiled in first batch of Polish translations

* Update messages.po

More translations.

* Compiled latest Polish translations

* Hindi, Tamil, Malayalam translation files

* Completed Swedish translation

* 80% complete

* Blank Finnish ("fi") translation file

* Compiled latest Swedish translations

* Full update and recompile of Swedish

* Compiled Polish translations

* Full update and recompile for Polish

* Update messages.po

* Updating Poedit docs

* Removed word wrap; compiled latest Dutch

* Hindi Translation --initial

* updated w/no line wrap; compiled first Hindi translations

* Update messages.po

* Update messages.po

Initial and partial test Bulgarian commit

* updated and compiled initial Bulgarian translations.

* Bugfix on text wrapping

* Update messages.po

* Update messages.po

BG translation ~25% complete

* Fully completed polish translation (first draft)

Probably needs some further tweaking here and there.

* Start Russian translation

* Russian translation 30% complete

* Format fixes

* Compiled latest French translations

* Compiled latest Chinese (Traditional) translations

* Compiled latest Bulgarian translations

* Compiled latest Polish translations

* Compiled latest Russian translations

* Update French to 36%

* Update messages.po

* Compiled in latest French translations

* Compiled latest Dutch translations

* Russian 70% complete

* Update messages.po

* Russian 100% complete

* Update French to 44%

Slowly but surely

* Adds in-progress languages; latest Russian translations compiled

* Extracted latest; updated Russian translation file.

* Compiled latest Chinese (Traditional) translations

* Updated and compiled Chinese (Traditional)

* Compiled latest Russian translations

* Updated and compiled latest French translations

* Updated all translation files

* Fix double quotes format issue

Co-authored-by: Kim Neunert <k9ert@gmx.de>
Co-authored-by: 2a3dex <58116837+2a3dex@users.noreply.github.com>
Co-authored-by: rafa1239 <raphael.altieri@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: Volker Herminghaus <volker@bitcorn.wtf>
Co-authored-by: glowleaf <69010742+glowleaf@users.noreply.github.com>
Co-authored-by: DirkVdk <86686185+DirkVdk@users.noreply.github.com>
Co-authored-by: mutatrum <mutatrum@gmail.com>
Co-authored-by: Kryptoministern <77808545+Kryptoministern@users.noreply.github.com>
Co-authored-by: PommbearBTC <86745004+PommbearBTC@users.noreply.github.com>
Co-authored-by: Gummybear <holuan@gmail.com>
Co-authored-by: sreshta suresh <sreshtasuresh@gmail.com>
Co-authored-by: KST-Energy <82534243+KST-Energy@users.noreply.github.com>
Co-authored-by: alltheseas <64376233+alltheseas@users.noreply.github.com>
Co-authored-by: dudezoo <86764634+dudezoo@users.noreply.github.com>
Co-authored-by: Sergei Tikhomirov <sergey.s.tikhomirov@gmail.com>
Co-authored-by: Bitpaint <67663265+bitpaint@users.noreply.github.com>
This commit is contained in:
kdmukai 2021-07-05 17:14:19 -05:00 committed by GitHub
parent 50493f2e6c
commit 4143cb88aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
148 changed files with 103626 additions and 1401 deletions

178
babel/README.md Normal file
View file

@ -0,0 +1,178 @@
# Babel multi-language support
## For Translators
Many of you translator volunteers will be new to git/github so we'll start with the basics:
### Create github account, install Github Desktop
Install the [Github Desktop app](https://desktop.github.com/). This is an easy way to manage your changes to an open source project.
When Github Desktop starts up it'll ask you to log in to your github account. If you don't already have an account, create one now.
### Clone the Specter Desktop repo
Now you're going to grab a copy of the full Specter Desktop source code, which includes the translation files.
Open the Github Desktop app and add the Specter Desktop repo via "Clone a Repository". On the "URL" tab type in ~~"cryptoadvance/specter-desktop"~~ (**for now we have to point to my work-in-progress repo: "kdmukai/specter-desktop"**):
<img src="img/add_repo.png">
Select where you want to save the source code on your local computer and hit "Clone".
When you're presented with the following choice, select "For my own purposes":
<img src="img/github_desktop_setup_01.jpg">
_When the clone is finished, click on the "Current Branch" button at the top middle and change to "origin/babel_integration" to point to my work-in-progress branch_
### Editing a translation file
Before you begin editing, ALWAYS go to Github Desktop and click the "Fetch origin" button at the top right. This pulls down all the latest changes from the repo. We'll all try our best to make sure that only one person is making changes at a time on a given translation file but if a change is submitted ahead of you, "Fetch origin" will grab it and get you in sync. We want to avoid messy conflicts where two or more people have edited the same translation file.
You'll be working with a `messages.po` file for each specific language. This file lists all the text that is displayed in Specter in English as a series of "msgid"s. Your job is to complete the matching translation in the "msgstr" field.
The `messages.po` text format is fairly straightforward but can be a little hard to work with, especially for multi-line phrases. Instead, we recommend that translators use the free tool [Poedit](https://poedit.net/download).
Once it's installed, go to Poedit preferences and uncheck "Wrap at":
<img src="img/poedit_01.png">
Navigate to the directory where you cloned the repo. The translation files can be found organized by two-letter language code (e.g. Spanish = "es") under `src/cryptoadvance/specter/translations`.
In Poedit open the `messages.po` file for your target langage (e.g. Spanish: [src/cryptoadvance/specter/translations/es/LC_MESSAGES/messages.po](../src/cryptoadvance/specter/translations/es/LC_MESSAGES/messages.po)).
Add or update translations, then save your changes.
### Editing Guidelines
* It's okay to leave some lines blank! If you can't provide a proper translation, just skip it and other translators will fill them in later. A blank entry will default to just displaying the original English.
* Some of the text strings have html or other code-related markup within them:
```
Please let us know! Create an <a href="https://github.com/cryptoadvance/specter-desktop/issues" target="_blank">issue</a> or ping us on <a href="https://t.me/spectersupport" target="_blank">Telegram</a> to fix it.
```
Your translations must preserve all of the html markup. If you are unfamiliar with html markup, feel free to just skip these entries.
* You'll also see passages with markers for where text will be dynamically inserted:
```
"Hello {}, welcome back!"
```
In this example case the user's name will be inserted in the curly brackets. Your translation must preserve the curly brackets.
This is used in a lot of our error reporting:
```
"Wallet failed to load. {}"
```
The brackets at the end will display an error code, debugging info, etc.
* Much of the text will include some very technical terms. If you're unfamiliar with the term in English or don't know its equivalent in your target language (e.g. "derivation path" or "hash"), review other occurrences of that word in existing translations in the `messages.po` file or ask in the Telegram group. Obviously we want to try to use the most recognizable, common term in each language, even though there won't necessarily be a standard set.
And many languages will most likely just keep many of the technical words the same (e.g. "xpub").
* The strings are presented out of context and a word may have many possible translations. If you can't figure out the right usage, ask for help in the Telegram group. Each string includes the exact line number where it was found in the code (right-click on it in Poedit and you'll see "Code Occurrences"). Include this info and others can help you get a better sense of how it's being used in that context. You can also directly review that line of code yourself (click on the entry in "Code Occurrences") but that may be bewildering if you are not a developer.
### Submitting your changes
The updated `messages.po` file should now show up in Github Desktop as having changes.
<img src="img/github_desktop_1.png">
_Note: You'll see the app offering to "create a fork". Do not create a fork! We want your local copy of the Specter Desktop code to remain connected to our repo, not a forked version that you'll have to manage._
**_The next steps will break from typical `git` procedures in order to keep things simple for our non-programmer translators._**
In a web browser go to github's latest version of the translation file you were just working on. Remember that the directories are organized by the two-letter language codes. So for Spanish ("es"):
[https://github.com/kdmukai/specter-desktop/tree/babel_integration/src/cryptoadvance/specter/translations/es/LC_MESSAGES/messages.po](https://github.com/kdmukai/specter-desktop/tree/babel_integration/src/cryptoadvance/specter/translations/es/LC_MESSAGES/messages.po)
Click on the pencil icon to edit the translation file:
<img src="img/github_edit_1.png">
You can now directly edit the file. But we want to use the version you just updated in Poedit.
On your computer, navigate to the `messages.po` file and open it with a simple text editor (macOS: TextEdit; Windows: Notepad). We have to make sure that the text editor doesn't change the file when it opens it (more complex editors might try to reformat the file). Copy the entire contents of the file.
Back in your browser, delete all the content in the editing window (click into the editing area and CTRL-A to select all, then delete), then paste your latest text into the online editing window.
If everything went correctly, you should just see your edits when you click the "Preview changes" tab just above the online editing window.
<img src="img/github_edit_2.png">
Any changed lines will be highlighted. Red highlights are deletions, green are additions.
Scroll to the bottom of that web page to the "Commit changes section". If your update addresses a specific issue that was raised in the repo's "Issues" section, reference it by number in the optional extended description box using this format: "Fixes #1234" (using the actual issue number, obv).
Select the option to "Create a new branch" and then click "Propose changes":
<img src="img/github_edit_3.png">
Then on the next screen follow the prompts to create your pull request and submit it to the project.
A reviewer on the Specter Desktop team will review your changes and (hopefully) accept the changes. Your contributions will be merged into the main Specter Desktop code repository. You will officially be a Specter Desktop open source contributor!
## For Developers:
### "Wrapping" text for translation
All you have to do in your code is wrap each piece of English text with the `ugettext` shorthand `_()`:
* Wrap jinja template text: `<p>Hello, world!</p>` becomes `<p>{{ _('Hello, world!') }}</p>`
* Wrap python strings: `error="No device was selected."` becomes `error=_("No device was selected.")`
* Use `.format()` to wrap strings with variable injections:
```
mystr = f"My dad's name is {dad.name} and my name is {self.name}."
mystr = _("My dad's name is {} and my name is {}").format(dad.name, self.name)
```
If there are a lot of variables to inject, placeholder names can be used:
```
mystr = _("My dad's name is {dad_name} and my name is {my_name}").format(dad_name=dad.name, my_name=self.name)
```
### Rescanning for text that needs translations
Re-generate the `messages.pot` file:
```
pybabel extract -F babel/babel.cfg -o babel/messages.pot .
```
This will rescan all wrapped text, picking up new strings as well as updating existings strings that have been edited.
Then run `update`:
```
pybabel update -N --no-wrap -i babel/messages.pot -d src/cryptoadvance/specter/translations
# Or target a specific language code:
pybabel update -N --no-wrap -i babel/messages.pot -d src/cryptoadvance/specter/translations -l es
```
_note: the `-N` flag prevents babel from trying to use fuzzy matching to re-use existing translations for new strings. The fuzzy matching does not seem to do what we would want so we keep it disabled._
Any newly wrapped text strings will be added to each `messages.po` file. Altered strings will be flagged as needing review to see if the existing translations can still be used.
Once the next round of translations is complete, recompile the results:
```
pybabel compile -d src/cryptoadvance/specter/translations
# Or target a specific language code:
pybabel compile -d src/cryptoadvance/specter/translations -l es
```
### Adding support for another language
Assuming you have `extract`ed an updated `messages.pot`, all you have to do is generate the initial version of each new language file:
```
pybabel init -i babel/messages.pot -d src/cryptoadvance/specter/translations -l es
pybabel init -i babel/messages.pot -d src/cryptoadvance/specter/translations -l fr
pybabel init -i babel/messages.pot -d src/cryptoadvance/specter/translations -l de
```
Then `update` and `compile` as above.
The only other step is to add the new language to the LANGUAGES list in `config.py`; the UI droplist automatically populates from that list. By convention use the native way to present the language:
```
# wrong:
"he": "Hebrew",
# right:
"he": "עברית",
```

4
babel/babel.cfg Normal file
View file

@ -0,0 +1,4 @@
[python: src/**/**.py]
[jinja2: src/**/templates/**.html]
[jinja2: src/**/templates/**.jinja]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

BIN
babel/img/add_repo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
babel/img/github_edit_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

BIN
babel/img/github_edit_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
babel/img/github_edit_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
babel/img/poedit_01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

4531
babel/messages.pot Normal file

File diff suppressed because it is too large Load diff

View file

@ -3,6 +3,7 @@ chardet==3.0.4
Click==7.1.2
daemonize==2.5.0
Flask==1.1.2
Flask-Babel==2.0.0
Flask-Cors==3.0.9
Flask-Login==0.5.0
hwi==2.0.1
@ -19,3 +20,4 @@ pyopenssl==20.0.1
flask_wtf==0.14.3
pgpy==0.5.3
cbor==1.0.0
mnemonic==0.19

View file

@ -1,9 +1,13 @@
#
# This file is autogenerated by pip-compile
# This file is autogenerated by pip-compile with python 3.8
# To update, run:
#
# pip-compile --generate-hashes requirements.in
#
babel==2.9.1 \
--hash=sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9 \
--hash=sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0
# via flask-babel
base58==2.0.1 \
--hash=sha256:365c9561d9babac1b5f18ee797508cd54937a724b6e419a130abad69cec5ca79 \
--hash=sha256:447adc750d6b642987ffc6d397ecd15a799852d5f6a1d308d384500243825058
@ -103,6 +107,19 @@ ecdsa==0.16.1 \
embit==0.4.2 \
--hash=sha256:d67fc0f7fbdb7588c3eb24441bf8e05770056260bc8e5537399a1b3ce5ccf12a
# via -r requirements.in
flask==1.1.2 \
--hash=sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060 \
--hash=sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557
# via
# -r requirements.in
# flask-babel
# flask-cors
# flask-login
# flask-wtf
flask-babel==2.0.0 \
--hash=sha256:e6820a052a8d344e178cdd36dd4bb8aea09b4bda3d5f9fa9f008df2c7f2f5468 \
--hash=sha256:f9faf45cdb2e1a32ea2ec14403587d4295108f35017a7821a2b1acb8cfd9257d
# via -r requirements.in
flask-cors==3.0.9 \
--hash=sha256:6bcfc100288c5d1bcb1dbb854babd59beee622ffd321e444b05f24d6d58466b8 \
--hash=sha256:cee4480aaee421ed029eaa788f4049e3e26d15b5affb6a880dade6bafad38324
@ -111,14 +128,6 @@ flask-login==0.5.0 \
--hash=sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b \
--hash=sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0
# via -r requirements.in
flask==1.1.2 \
--hash=sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060 \
--hash=sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557
# via
# -r requirements.in
# flask-cors
# flask-login
# flask-wtf
flask_wtf==0.14.3 \
--hash=sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2 \
--hash=sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720
@ -169,7 +178,9 @@ itsdangerous==1.1.0 \
jinja2==2.11.3 \
--hash=sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419 \
--hash=sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6
# via flask
# via
# flask
# flask-babel
libusb1==1.8 \
--hash=sha256:240f65ac70ba3fab77749ec84a412e4e89624804cb80d6c9d394eef5af8878d6
# via hwi
@ -210,15 +221,15 @@ markupsafe==1.1.1 \
# via
# jinja2
# wtforms
mnemonic==0.18 \
--hash=sha256:02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d
# via hwi
mnemonic==0.19 \
--hash=sha256:4e37eb02b2cbd56a0079cabe58a6da93e60e3e4d6e757a586d9f23d96abea931 \
--hash=sha256:a8d78c5100acfa7df9bab6b9db7390831b0e54490934b718ff9efd68f0d731a6
# via
# -r requirements.in
# hwi
noiseprotocol==0.3.1 \
--hash=sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111
# via bitbox02
pbkdf2==1.3 \
--hash=sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979
# via mnemonic
pgpy==0.5.3 \
--hash=sha256:a49c269cedcaf82ac6999bcae5fd3f543ecb1c759f9d48a15ad8d8fa4ac03987 \
--hash=sha256:cba6fbbb44a896a8a4f5807b3d8d4943a8f7a6607be11587f4a27734c711c1dd
@ -284,6 +295,12 @@ python-dotenv==0.13.0 \
--hash=sha256:25c0ff1a3e12f4bde8d592cc254ab075cfe734fc5dd989036716fd17ee7e5ec7 \
--hash=sha256:3b9909bc96b0edc6b01586e1eed05e71174ef4e04c71da5786370cebea53ad74
# via -r requirements.in
pytz==2021.1 \
--hash=sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da \
--hash=sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798
# via
# babel
# flask-babel
requests==2.25.0 \
--hash=sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8 \
--hash=sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998

View file

@ -104,6 +104,24 @@ class BaseConfig(object):
},
}
# Babel integration. English listed first; other alphabetical by language code
LANGUAGES = {
"en": "English",
"bg": "Български",
"de": "Deutsch",
"el": "Ελληνικά",
"es": "Español",
"fr": "Français",
"he": "עברית",
"hi": "हिंदी",
"nl": "Nederlands",
"pl": "Polski",
"ru": "Русский",
"sv": "Svenska",
"zh_Hans_CN": "简体中文",
"zh_Hant_TW": "繁體中文",
}
class DevelopmentConfig(BaseConfig):
# https://stackoverflow.com/questions/22463939/demystify-flask-app-secret-key
@ -112,6 +130,9 @@ class DevelopmentConfig(BaseConfig):
os.getenv("SPECTER_DATA_FOLDER", "~/.specter_dev")
)
# Env vars take priority over config settings so ensure that this is set
os.environ["FLASK_ENV"] = "development"
class TestConfig(BaseConfig):
SECRET_KEY = "test key"

View file

@ -265,11 +265,39 @@ def get_startblock_by_chain(specter):
# Hot wallet helpers
def generate_mnemonic(strength=256):
# Generate words list
mnemo = Mnemonic("english")
words = mnemo.generate(strength=strength)
return words
MNEMONIC_LANGUAGES = {
"en": "english",
"es": "spanish",
"fr": "french",
"it": "italian",
# "jp": "japanese",
# "ko": korean",
# "?": chinese_simplified",
# "?": chinese_traditional",
}
def initialize_mnemonic(language_code):
if language_code not in MNEMONIC_LANGUAGES:
# Fall back to English if Mnemonic doesn't support the current language
logger.debug(
f"Language code '{language_code}' not supported by python-mnemonic; using English"
)
language_code = "en"
return Mnemonic(language=MNEMONIC_LANGUAGES[language_code])
def generate_mnemonic(strength=256, language_code="en"):
mnemo = initialize_mnemonic(language_code)
return mnemo.generate(strength=strength)
def validate_mnemonic(words):
# We cannot assume the mnemonic will be in the same language currently active
# in the UI (e.g. a Spanish user is likely to have an English mnemonic).
mnemo = initialize_mnemonic(Mnemonic.detect_language(words))
return mnemo.check(words)
def notify_upgrade(app, flash):

View file

@ -1,6 +1,8 @@
import os
import json
import logging
from flask_babel import lazy_gettext as _
from ..helpers import alias, load_jsons
from ..rpc import get_default_datadir
@ -79,7 +81,7 @@ class DeviceManager:
for device_name in self.devices:
if self.devices[device_name].alias == device_alias:
return self.devices[device_name]
logger.error("Could not find Device %s" % device_alias)
logger.error(_("Could not find Device {}").format(device_alias))
def remove_device(
self,

View file

@ -3,6 +3,7 @@ import logging
import os
from embit.liquid.networks import get_network
from flask_babel import lazy_gettext as _
from .helpers import is_testnet, is_liquid
from .liquid.rpc import LiquidRPC
from .persistence import write_node
@ -312,7 +313,7 @@ class Node:
"""
rpc = self.get_rpc()
if rpc is None:
return {"out": "", "err": "Connection to node failed", "code": -1}
return {"out": "", "err": _("Connection to node failed"), "code": -1}
r = {}
r["tests"] = {"connectable": False}
r["err"] = ""
@ -322,7 +323,7 @@ class Node:
int(rpc.getnetworkinfo()["version"]) >= 200000
)
if not r["tests"]["recent_version"]:
r["err"] = "Core Node might be too old"
r["err"] = _("Core Node might be too old")
r["tests"]["connectable"] = True
r["tests"]["credentials"] = True
@ -339,7 +340,7 @@ class Node:
logger.error("Caught an ConnectionError while test_rpc: %s", e)
r["tests"]["connectable"] = False
r["err"] = "Failed to connect!"
r["err"] = _("Failed to connect!")
r["code"] = -1
except RpcError as rpce:
logger.error("Caught an RpcError while test_rpc: %s", rpce)
@ -348,7 +349,7 @@ class Node:
r["code"] = rpc.r.status_code
if rpce.status_code == 401:
r["tests"]["credentials"] = False
r["err"] = "RPC authentication failed!"
r["err"] = _("RPC authentication failed!")
else:
r["err"] = str(rpce.status_code)
except Exception as e:
@ -362,7 +363,7 @@ class Node:
r["err"] = rpc.r["error"]
r["code"] = rpc.r.status_code
else:
r["err"] = "Failed to connect"
r["err"] = _("Failed to connect")
r["code"] = -1
return r

View file

@ -5,7 +5,8 @@ import secrets
from pathlib import Path
from dotenv import load_dotenv
from flask import Flask, redirect, url_for
from flask import Flask, redirect, request, url_for, jsonify, session
from flask_babel import Babel
from flask_login import LoginManager, login_user
from flask_wtf.csrf import CSRFProtect
@ -26,6 +27,26 @@ load_dotenv(env_path)
csrf = CSRFProtect()
class SpecterFlask(Flask):
@property
def supported_languages(self):
return self.config["LANGUAGES"]
def get_language_code(self):
"""
Helper for Babel and other related language selection tasks.
"""
if "language_code" in session:
# Explicit selection
return session["language_code"]
else:
# autodetect
return request.accept_languages.best_match(self.supported_languages.keys())
def set_language_code(self, language_code):
session["language_code"] = language_code
def calc_module_name(config):
"""tiny helper to make passing configs more convenient"""
if "." in config:
@ -58,11 +79,13 @@ def create_app(config=None):
template_folder = os.path.join(sys._MEIPASS, "templates")
static_folder = os.path.join(sys._MEIPASS, "static")
logger.info("pyinstaller based instance running in {}".format(sys._MEIPASS))
app = Flask(
app = SpecterFlask(
__name__, template_folder=template_folder, static_folder=static_folder
)
else:
app = Flask(__name__, template_folder="templates", static_folder="static")
app = SpecterFlask(
__name__, template_folder="templates", static_folder="static"
)
app.jinja_env.autoescape = select_autoescape(default_for_string=True, default=True)
logger.info(f"Configuration: {config}")
app.config.from_object(config)
@ -71,6 +94,7 @@ def create_app(config=None):
)
csrf.init_app(app)
app.csrf = csrf
return app
@ -142,6 +166,28 @@ def init_app(app, hwibridge=False, specter=None):
return dict(tor_service_id="", tor_enabled=False)
return dict(tor_service_id=app.tor_service_id, tor_enabled=app.tor_enabled)
# --------------------- Babel integration ---------------------
babel = Babel(app)
@babel.localeselector
def get_language_code():
# Enables Babel to auto-detect current language
return app.get_language_code()
@app.route("/set_language", methods=["POST"])
def set_language_code():
json_data = request.get_json()
if (
"language_code" in json_data
and json_data["language_code"] in app.supported_languages
):
app.set_language_code(json_data["language_code"])
return jsonify(success=True)
else:
return jsonify(success=False)
# --------------------- Babel integration ---------------------
return app

View file

@ -9,6 +9,7 @@ from flask import (
jsonify,
flash,
)
from flask_babel import lazy_gettext as _
from flask_login import login_required, current_user, logout_user
from flask import current_app as app
from ..helpers import alias
@ -46,7 +47,9 @@ def login():
)
return redirect_login(request)
flash(
"We could not check your password, maybe Bitcoin Core is not running or not configured?",
_(
"We could not check your password, maybe Bitcoin Core is not running or not configured?"
),
"error",
)
app.logger.info("AUDIT: Failed to check password")
@ -79,7 +82,7 @@ def login():
app.login(user.id)
return redirect_login(request)
# Either invalid method or incorrect credentials
flash("Invalid username or password", "error")
flash(_("Invalid username or password"), "error")
app.logger.info("AUDIT: Invalid password login attempt")
return (
render_template(
@ -108,14 +111,16 @@ def register():
otp = request.form["otp"]
if not username:
flash(
"Please enter a username.",
_("Please enter a username."),
"error",
)
return redirect("register?otp={}".format(otp))
min_chars = int(app.specter.config["auth"]["password_min_chars"])
if not password or len(password) < min_chars:
flash(
"Please enter a password of a least {} characters.".format(min_chars),
_("Please enter a password of a least {} characters.").format(
min_chars
),
"error",
)
return redirect("register?otp={}".format(otp))
@ -126,7 +131,9 @@ def register():
i += 1
user_id = "{}{}".format(alias(username), i)
if app.specter.user_manager.get_user_by_username(username):
flash("Username is already taken, please choose another one", "error")
flash(
_("Username is already taken, please choose another one"), "error"
)
return redirect("register?otp={}".format(otp))
app.specter.otp_manager.remove_new_user_otp(otp)
config = {
@ -137,14 +144,18 @@ def register():
user = User(user_id, username, password_hash, config, app.specter)
app.specter.user_manager.add_user(user)
flash(
"You have registered successfully, \
_(
"You have registered successfully, \
please login with your new account to start using Specter"
)
)
return redirect(url_for("auth_endpoint.login"))
else:
flash(
"Invalid registration link, \
please request a new link from the node operator.",
_(
"Invalid registration link, \
please request a new link from the node operator."
),
"error",
)
return redirect("register?otp={}".format(otp))
@ -154,13 +165,13 @@ please request a new link from the node operator.",
@auth_endpoint.route("/logout", methods=["GET", "POST"])
def logout():
logout_user()
flash("You were logged out", "info")
flash(_("You were logged out"), "info")
return redirect(url_for("auth_endpoint.login"))
################### Util ######################
def redirect_login(request):
flash("Logged in successfully.", "info")
flash(_("Logged in successfully."), "info")
if request.form.get("next") and request.form.get("next") != "None":
response = redirect(request.form["next"])
else:

View file

@ -5,6 +5,7 @@ from flask import make_response
from flask_wtf.csrf import CSRFError
from werkzeug.exceptions import MethodNotAllowed
from flask import render_template, request, redirect, url_for, flash, Markup
from flask_babel import lazy_gettext as _
from flask_login import login_required, current_user
from ..helpers import (
generate_mnemonic,
@ -50,10 +51,11 @@ def server_rpc_error(rpce):
"""Specific SpecterErrors get passed on to the User as flash"""
if rpce.error_code == -18: # RPC_WALLET_NOT_FOUND
flash(
"Wallet not found. Specter reloaded all Wallets, please try again.", "error"
_("Wallet not found. Specter reloaded all wallets, please try again."),
"error",
)
else:
flash(f"BitcoinCore RpcError: {str(rpce)}", "error")
flash(_("Bitcoin Core RpcError: {}").format(str(rpce)), "error")
try:
app.specter.wallet_manager.update()
except SpecterError as se:
@ -94,7 +96,9 @@ def server_error_timeout(e):
app.specter.check()
app.logger.error("ExternalProcessTimeoutException: %s" % e)
flash(
"Bitcoin Core is not coming up in time. Maybe it's just slow but please check the logs below",
_(
"Bitcoin Core is not coming up in time. Maybe it's just slow but please check the logs below"
),
"warn",
)
return redirect(
@ -113,7 +117,7 @@ def server_error_csrf(e):
app.logger.error("CSRF Exception: %s" % e)
trace = traceback.format_exc()
app.logger.error(trace)
flash("Session expired. Please refresh and try again.", "error")
flash(_("Session expired. Please refresh and try again."), "error")
return redirect(request.url)
@ -123,7 +127,7 @@ def server_error_405(e):
app.logger.error("405 MethodNotAllowed Exception: %s" % e)
trace = traceback.format_exc()
app.logger.error(trace)
flash("Session expired. Please refresh and try again.", "error")
flash(_("Session expired. Please refresh and try again."), "error")
return redirect(request.url)
@ -180,7 +184,12 @@ def about():
app.specter.reset_setup("bitcoind")
app.specter.reset_setup("torbrowser")
return render_template("base.jinja", specter=app.specter, rand=rand)
return render_template(
"base.jinja",
specter=app.specter,
rand=rand,
supported_languages=app.supported_languages,
)
# TODO: Move all these below to REST API
@ -206,7 +215,12 @@ def wallets_loading():
@app.route("/generatemnemonic/", methods=["GET", "POST"])
@login_required
def generatemnemonic():
return {"mnemonic": generate_mnemonic(strength=int(request.form["strength"]))}
return {
"mnemonic": generate_mnemonic(
strength=int(request.form["strength"]),
language_code=app.get_language_code(),
)
}
################ RPC data utils ####################
@ -300,5 +314,7 @@ def get_whitepaper():
else:
return render_template(
"500.jinja",
error="You need a mainnet node to retrieve the whitepaper. Check your node configurations.",
error=_(
"You need a mainnet node to retrieve the whitepaper. Check your node configurations."
),
)

View file

@ -10,13 +10,14 @@ from flask import (
jsonify,
flash,
)
from flask_login import login_required, current_user
from flask import current_app as app
from flask_babel import lazy_gettext as _
from flask_login import login_required, current_user
from mnemonic import Mnemonic
from ..helpers import is_testnet, generate_mnemonic
from ..devices.bitcoin_core import BitcoinCore
from ..helpers import is_testnet, generate_mnemonic, validate_mnemonic
from ..key import Key
from ..managers.device_manager import get_device_class
from ..devices.bitcoin_core import BitcoinCore
from ..managers.wallet_manager import purposes
from ..specter_error import handle_exception
@ -60,9 +61,9 @@ def new_device_keys(device_type):
else:
device_name = request.form.get("device_name", "")
if not device_name:
err = "Device name must not be empty"
err = _("Device name cannot be empty")
elif device_name in app.specter.device_manager.devices_names:
err = "Device with this name already exists"
err = _("Device with this name already exists")
xpubs_rows_count = int(request.form["xpubs_rows_count"]) + 1
keys = []
paths = []
@ -80,12 +81,12 @@ def new_device_keys(device_type):
try:
keys.append(Key.parse_xpub(xpub, purpose=purpose))
except:
err = "Failed to parse these xpubs:\n" + "\n".join(xpub)
err = _("Failed to parse these xpubs") + ":\n" + "\n".join(xpub)
break
if not keys and not err:
if device_type in ["bitcoincore", "elementscore"]:
if not paths:
err = "No paths were specified, please provide at lease one."
err = _("No paths were specified, please provide at least one.")
if err is None:
if existing_device:
device.add_hot_wallet_keys(
@ -98,7 +99,7 @@ def new_device_keys(device_type):
keys_range=[range_start, range_end],
keys_purposes=keys_purposes,
)
flash("{} keys were added successfully".format(len(paths)))
flash(_("{} keys were added successfully").format(len(paths)))
return redirect(
url_for(
"devices_endpoint.device", device_alias=device.alias
@ -119,7 +120,7 @@ def new_device_keys(device_type):
keys_range=[range_start, range_end],
keys_purposes=keys_purposes,
)
flash("{} was added successfully!".format(device_name))
flash(_("{} was added successfully!").format(device_name))
return redirect(
url_for(
"devices_endpoint.device", device_alias=device.alias
@ -128,7 +129,10 @@ def new_device_keys(device_type):
)
except Exception as e:
handle_exception(e)
flash(f"Failed to setup hot wallet. Error: {e}", "error")
flash(
_("Failed to setup hot wallet. Error: {}").format(e),
"error",
)
app.specter.device_manager.remove_device(
device,
app.specter.wallet_manager,
@ -136,11 +140,11 @@ def new_device_keys(device_type):
chain=app.specter.chain,
)
else:
err = "xpubs list must not be empty"
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)))
flash(_("{} keys were added successfully").format(len(keys)))
return redirect(
url_for("devices_endpoint.device", device_alias=device.alias)
)
@ -157,7 +161,7 @@ def new_device_keys(device_type):
rand=rand,
)
else:
flash("{} was added successfully!".format(device_name))
flash(_("{} was added successfully!").format(device_name))
return redirect(
url_for("devices_endpoint.device", device_alias=device.alias)
+ "?newdevice=true"
@ -185,18 +189,21 @@ def new_device_keys(device_type):
def new_device_mnemonic(device_type):
err = None
strength = 128
mnemonic = generate_mnemonic(strength=strength)
mnemonic = generate_mnemonic(
strength=strength, language_code=app.get_language_code()
)
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."
err = _(
"Invalid mnemonic entered: Must contain either: 12, 15, 18, 21, or 24 words."
)
if not validate_mnemonic(words=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."
err = _("Invalid address range selected.")
mnemonic = request.form["mnemonic"]
passphrase = request.form["passphrase"]
file_password = request.form["file_password"]
@ -238,7 +245,7 @@ def device_blinding_key(device_alias):
device = app.specter.device_manager.get_by_alias(device_alias)
except:
return render_template(
"base.jinja", error="Device not found", specter=app.specter, rand=rand
"base.jinja", error=_("Device not found"), specter=app.specter, rand=rand
)
if not device:
return redirect(url_for("index"))
@ -247,7 +254,7 @@ def device_blinding_key(device_alias):
blinding_key = request.form.get("blinding_key")
device.set_blinding_key(blinding_key)
if not new_device:
flash("Master blinding key was added successfully")
flash(_("Master blinding key was added successfully"))
return redirect(
url_for("devices_endpoint.device", device_alias=device.alias)
+ ("?newdevice=true" if new_device else "")
@ -271,22 +278,24 @@ def new_device_manual():
device_name = ""
xpubs = ""
strength = 128
mnemonic = generate_mnemonic(strength=strength)
mnemonic = generate_mnemonic(
strength=strength, language_code=app.get_language_code()
)
if request.method == "POST":
action = request.form["action"]
device_type = request.form["device_type"]
device_name = request.form["device_name"]
if action == "newcolddevice":
if not device_name:
err = "Device name must not be empty"
err = _("Device name cannot be empty")
elif device_name in app.specter.device_manager.devices_names:
err = "Device with this name already exists"
err = _("Device with this name already exists")
xpubs = request.form["xpubs"]
if not xpubs:
err = "xpubs name must not be empty"
err = _("xpubs name cannot be empty")
keys, failed = Key.parse_xpubs(xpubs)
if len(failed) > 0:
err = "Failed to parse these xpubs:\n" + "\n".join(failed)
err = _("Failed to parse these xpubs") + ":\n" + "\n".join(failed)
if err is None:
device = app.specter.device_manager.add_device(
name=device_name, device_type=device_type, keys=keys
@ -296,18 +305,20 @@ def new_device_manual():
)
elif action == "newhotdevice":
if not device_name:
err = "Device name must not be empty"
err = _("Device name cannot be empty")
elif device_name in app.specter.device_manager.devices_names:
err = "Device with this name already exists"
err = _("Device with this name already exists")
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."
err = _(
"Invalid mnemonic entered: Must contain either: 12, 15, 18, 21, or 24 words."
)
if not validate_mnemonic(words=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."
err = _("Invalid address range selected.")
if err is None:
mnemonic = request.form["mnemonic"]
paths = [
@ -336,7 +347,7 @@ def new_device_manual():
)
except Exception as e:
handle_exception(e)
flash(f"Failed to setup hot wallet. Error: {e}", "error")
flash(_("Failed to setup hot wallet. Error: {}").format(e), "error")
app.specter.device_manager.remove_device(
device,
app.specter.wallet_manager,
@ -345,7 +356,9 @@ def new_device_manual():
)
elif action == "generatemnemonic":
strength = int(request.form["strength"])
mnemonic = generate_mnemonic(strength=strength)
mnemonic = generate_mnemonic(
strength=strength, language_code=app.get_language_code()
)
return render_template(
"device/new_device_manual.jinja",
device_type=device_type,
@ -370,7 +383,7 @@ def device(device_alias):
device = app.specter.device_manager.get_by_alias(device_alias)
except:
return render_template(
"base.jinja", error="Device not found", specter=app.specter, rand=rand
"base.jinja", error=_("Device not found"), specter=app.specter, rand=rand
)
if not device:
return redirect(url_for("index"))
@ -379,8 +392,17 @@ def device(device_alias):
action = request.form["action"]
if action == "forget":
if len(wallets) != 0:
err = "Device could not be removed since it is used in wallets: {}.<br>You must delete those wallets before you can remove this device.<br>You can delete a wallet from its Settings -> Advanced page.".format(
[wallet.name for wallet in wallets]
# TODO: Long message strings like this should be moved into a template.
err = (
_(
"Device could not be removed since it is used in wallets: {}"
).format([wallet.name for wallet in wallets])
+ "<br>"
+ _(
"You must delete those wallets before you can remove this device."
)
+ "<br>"
+ _("You can delete a wallet from its Settings -> Advanced page.")
)
else:
app.specter.device_manager.remove_device(
@ -394,24 +416,33 @@ def device(device_alias):
key = Key.from_json({"original": request.form["key"]})
wallets_with_key = [w for w in wallets if key in w.keys]
if len(wallets_with_key) != 0:
err = "Key could not be removed since it is used in wallets: {}.<br>You must delete those wallets before you can remove this key.<br>You can delete a wallet from its Settings -> Advanced page.".format(
", ".join([wallet.name for wallet in wallets_with_key])
# TODO: Long message strings like this should be moved into a template.
err = (
_(
"Key could not be removed since it is used in wallets: {}"
).format(", ".join([wallet.name for wallet in wallets_with_key]))
+ "<br>"
+ _("You must delete those wallets before you can remove this key.")
+ "<br>"
+ _("You can delete a wallet from its Settings -> Advanced page.")
)
else:
device.remove_key(key)
elif action == "rename":
device_name = request.form["newtitle"]
if not device_name:
flash("Device name must not be empty", "error")
flash(_("Device name cannot be empty"), "error")
elif device_name == device.name:
pass
elif device_name in app.specter.device_manager.devices_names:
flash("Device already exists", "error")
flash(_("Device already exists"), "error")
else:
device.rename(device_name)
elif action == "add_keys":
strength = 128
mnemonic = generate_mnemonic(strength=strength)
mnemonic = generate_mnemonic(
strength=strength, language_code=app.get_language_code()
)
if device.hot_wallet:
return render_template(
"device/new_device/new_device_mnemonic.jinja",

View file

@ -10,6 +10,7 @@ from flask import (
jsonify,
flash,
)
from flask_babel import lazy_gettext as _
from flask_login import login_required, current_user
from flask import current_app as app
from ..rpc import get_default_datadir
@ -43,7 +44,7 @@ def node_settings(node_alias):
)
except:
return render_template(
"base.jinja", error="Node not found", specter=app.specter, rand=rand
"base.jinja", error=_("Node not found"), specter=app.specter, rand=rand
)
else:
node = Node.from_json(
@ -62,7 +63,7 @@ def node_settings(node_alias):
)
if not current_user.is_admin:
flash("Only an admin is allowed to access this page.", "error")
flash(_("Only an admin is allowed to access this page"), "error")
return redirect("")
# The node might have been down but is now up again
# (and the checker did not realized yet) and the user clicked "Configure Node"
@ -97,19 +98,19 @@ def node_settings(node_alias):
if action == "rename":
node_name = request.form["newtitle"]
if not node_name:
flash("Node name must not be empty", "error")
flash(_("Node name cannot be empty"), "error")
elif node_name == node.name:
pass
elif node_name in app.specter.device_manager.devices_names:
flash("Node with this name already exists", "error")
flash(_("Node with this name already exists"), "error")
else:
node.rename(node_name)
elif action == "forget":
if not node_alias:
flash("Failed to deleted node. Node isn't saved", "error")
flash(_("Failed to deleted node. Node isn't saved"), "error")
elif len(app.specter.node_manager.nodes) > 1:
app.specter.node_manager.delete_node(node, app.specter)
flash("Node deleted successfully")
flash(_("Node deleted successfully"))
return redirect(
url_for(
"nodes_endpoint.node_settings",
@ -118,7 +119,9 @@ def node_settings(node_alias):
)
else:
flash(
"Failed to deleted node. Specter must have at least one node configured",
_(
"Failed to delete node. Specter must have at least one node configured"
),
"error",
)
elif action == "test":
@ -144,16 +147,18 @@ def node_settings(node_alias):
if "tests" in test:
# If any test has failed, we notify the user that the test has not passed
if False in list(test["tests"].values()):
flash(f"Test failed: {test['err']}", "error")
flash(_("Test failed: {}").format(test["err"]), "error")
else:
flash("Test passed", "info")
flash(_("Test passed"), "info")
elif "err" in test:
flash(f"Test failed: {test['err']}", "error")
flash(_("Test failed: {}").format(test["err"]), "error")
elif action == "save":
if not node_alias:
if node.name in app.specter.node_manager.nodes:
flash(
"Node with this name already exits, please choose a different name.",
_(
"Node with this name already exists, please choose a different name."
),
"error",
)
return render_template(
@ -190,7 +195,7 @@ def node_settings(node_alias):
protocol=protocol,
)
if not success:
flash("Failed connecting to the node", "error")
flash(_("Failed connecting to the node"), "error")
if app.specter.active_node_alias == node.alias:
app.specter.check()
@ -220,7 +225,7 @@ def internal_node_settings(node_alias):
)
except:
return render_template(
"base.jinja", error="Node not found", specter=app.specter, rand=rand
"base.jinja", error=_("Node not found"), specter=app.specter, rand=rand
)
else:
# TODO: Allow internal node setup here?
@ -232,7 +237,7 @@ def internal_node_settings(node_alias):
)
if not current_user.is_admin:
flash("Only an admin is allowed to access this page.", "error")
flash(_("Only an admin is allowed to access this page"), "error")
return redirect("")
# The node might have been down but is now up again
# (and the checker did not realized yet) and the user clicked "Configure Node"
@ -245,7 +250,7 @@ def internal_node_settings(node_alias):
if action == "rename":
node_name = request.form["newtitle"]
if not node_name:
flash("Node name must not be empty", "error")
flash(_("Node name must not be empty"), "error")
elif node_name == node.name:
pass
elif node_name in app.specter.device_manager.devices_names:
@ -254,13 +259,13 @@ def internal_node_settings(node_alias):
node.rename(node_name)
elif action == "forget":
if not node_alias:
flash("Failed to deleted node. Node isn't saved", "error")
flash(_("Failed to deleted node. Node isn't saved"), "error")
elif len(app.specter.node_manager.nodes) > 1:
node.stop()
app.specter.node_manager.delete_node(node, app.specter)
if bool(request.form.get("remove_datadir", False)):
shutil.rmtree(os.path.expanduser(node.datadir), ignore_errors=True)
flash("Node deleted successfully")
flash(_("Node deleted successfully"))
return redirect(
url_for(
"nodes_endpoint.node_settings",
@ -269,27 +274,29 @@ def internal_node_settings(node_alias):
)
else:
flash(
"Failed to deleted node. Specter must have at least one node configured",
_(
"Failed to delete node. Specter must have at least one node configured"
),
"error",
)
elif action == "stopbitcoind":
try:
node.stop()
time.sleep(5)
flash("Specter stopped Bitcoin Core successfully")
flash(_("Specter successfully stopped Bitcoin Core"))
except Exception as e:
try:
logger.exception(e)
flash("Stopping Bitcoin Core, this might take a few moments.")
flash(_("Stopping Bitcoin Core, this might take a few moments."))
node.rpc.stop()
except Exception as ne:
logger.exception(ne)
flash(f"Failed to stop Bitcoin Core {ne}", "error")
flash(_("Failed to stop Bitcoin Core {}").format(ne), "error")
elif action == "startbitcoind":
if node.start(timeout=120):
flash("Specter has started Bitcoin Core")
flash(_("Specter has started Bitcoin Core"))
else:
flash("Specter failed to start the node...", "error")
flash(_("Specter failed to start the node"), "error")
elif action == "uninstall_bitcoind":
try:
node.stop()
@ -299,7 +306,7 @@ def internal_node_settings(node_alias):
)
if bool(request.form.get("remove_datadir", False)):
shutil.rmtree(os.path.expanduser(node.datadir), ignore_errors=True)
flash(f"Bitcoin Core uninstalled successfully")
flash(_("Bitcoin Core successfully uninstalled"))
app.specter.node_manager.delete_node(node, app.specter)
return redirect(
url_for(
@ -308,7 +315,7 @@ def internal_node_settings(node_alias):
)
)
except Exception as e:
flash(f"Failed to remove Bitcoin Core, error: {e}", "error")
flash(_("Failed to remove Bitcoin Core, error: {}").format(e), "error")
elif action == "upgrade_bitcoind":
if node.version != app.config["INTERNAL_BITCOIND_VERSION"]:
try:
@ -317,10 +324,13 @@ def internal_node_settings(node_alias):
)
except Exception as e:
flash(
f"Failed to upgrade Bitcoin Core version, error: {e}", "error"
_("Failed to upgrade Bitcoin Core version, error: {}").format(
e
),
"error",
)
else:
flash("Bitcoin Core version is already up to date")
flash(_("Bitcoin Core version is already up to date"))
return render_template(
"node/internal_node_settings.jinja",
node=node,

View file

@ -16,6 +16,7 @@ import requests
from flask import Blueprint, Flask
from flask import current_app as app
from flask import flash, jsonify, redirect, render_template, request, send_file, url_for
from flask_babel import lazy_gettext as _
from flask_login import current_user, login_required
from ..helpers import (
@ -108,7 +109,7 @@ def general():
# continue with the existing one
if "already exists" not in str(e):
flash(
"Failed to import wallet {}, error: {}".format(
_("Failed to import wallet {}, error: {}").format(
wallet["name"], e
),
"error",
@ -142,16 +143,21 @@ def general():
)
)
flash(
"Failed to perform rescan for wallet: {}".format(e), "error"
_("Failed to perform rescan for wallet: {}").format(e),
"error",
)
wallet_obj.getdata()
except Exception:
flash("Failed to import wallet {}".format(wallet["name"]), "error")
flash("Specter data was successfully loaded from backup.", "info")
flash(
_("Failed to import wallet {}").format(wallet["name"]), "error"
)
flash(_("Specter data was successfully loaded from backup"), "info")
if rescanning:
flash(
"Wallets are rescanning for transactions history.\n\
This may take a few hours to complete.",
_(
"Wallets are rescanning for transactions history.\n\
This may take a few hours to complete."
),
"info",
)
@ -165,6 +171,7 @@ This may take a few hours to complete.",
specter=app.specter,
current_version=current_version,
rand=rand,
supported_languages=app.supported_languages,
)
@ -179,7 +186,7 @@ def tor():
param only_tor "on" or something else ("off")
"""
if not current_user.is_admin:
flash("Only an admin is allowed to access this page.", "error")
flash(_("Only an admin is allowed to access this page."), "error")
return redirect("")
app.specter.reset_setup("torbrowser")
current_version = notify_upgrade(app, flash)
@ -250,18 +257,18 @@ def tor():
logger.info("Starting Tor...")
try:
app.specter.tor_daemon.start_tor_daemon()
flash("Specter has started Tor")
flash(_("Specter has started Tor"))
except Exception as e:
flash(f"Failed to start Tor, error: {e}", "error")
flash(_("Failed to start Tor, error: {}").format(e), "error")
logger.error(f"Failed to start Tor, error: {e}")
elif action == "stoptor":
logger.info("Stopping Tor...")
try:
app.specter.tor_daemon.stop_tor_daemon()
time.sleep(1)
flash("Specter stopped Tor successfully")
flash(_("Specter stopped Tor successfully"))
except Exception as e:
flash(f"Failed to stop Tor, error: {e}", "error")
flash(_("Failed to stop Tor, error: {}").format(e), "error")
logger.error(f"Failed to start Tor, error: {e}")
elif action == "uninstalltor":
logger.info("Uninstalling Tor...")
@ -270,9 +277,9 @@ def tor():
app.specter.tor_daemon.stop_tor_daemon()
shutil.rmtree(os.path.join(app.specter.data_folder, "tor-binaries"))
os.remove(os.path.join(app.specter.data_folder, "torrc"))
flash(f"Tor uninstalled successfully")
flash(_("Tor uninstalled successfully"))
except Exception as e:
flash(f"Failed to uninstall Tor, error: {e}", "error")
flash(_("Failed to uninstall Tor, error: {}").format(e), "error")
logger.error(f"Failed to uninstall Tor, error: {e}")
elif action == "test_tor":
logger.info("Testing the Tor connection...")
@ -287,10 +294,12 @@ def tor():
)
tor_connectable = res.status_code == 200
if tor_connectable:
flash("Tor requests test completed successfully!", "info")
flash(_("Tor requests test completed successfully!"), "info")
else:
flash(
f"Failed to make test request over Tor. Status-Code: {res.status_code}",
_(
"Failed to make test request over Tor. Status-Code: {}"
).format(res.status_code),
"error",
)
logger.error(
@ -303,7 +312,10 @@ def tor():
logger.error(app.specter.tor_daemon.get_logs())
app.specter.tor_daemon.start_tor_daemon()
except Exception as e:
flash(f"Failed to make test request over Tor.\nError: {e}", "error")
flash(
_("Failed to make test request over Tor.\nError: {}").format(e),
"error",
)
logger.error(f"Failed to make test request over Tor.\nError: {e}")
if tor_type == "builtin":
logger.error("Tor-Logs:")
@ -358,7 +370,7 @@ def auth():
if current_user.username != specter_username:
if app.specter.user_manager.get_user_by_username(specter_username):
flash(
"Username is already taken, please choose another one",
_("Username is already taken, please choose another one"),
"error",
)
return render_template(
@ -375,9 +387,9 @@ def auth():
if specter_password:
if len(specter_password) < min_chars:
flash(
"Please enter a password of a least {} characters.".format(
min_chars
),
_(
"Please enter a password of a least {} characters"
).format(min_chars),
"error",
)
return render_template(
@ -400,9 +412,9 @@ def auth():
if new_password:
if len(new_password) < min_chars:
flash(
"Please enter a password of a least {} characters.".format(
min_chars
),
_(
"Please enter a password of a least {} characters"
).format(min_chars),
"error",
)
return render_template(
@ -441,9 +453,9 @@ def auth():
if timeout > 0:
expiry = now + timeout * 60 * 60
if timeout > 1:
expiry_desc = " (expires in {} hours)".format(timeout)
expiry_desc = " " + _("(expires in {} hours)").format(timeout)
else:
expiry_desc = " (expires in 1 hour)"
expiry_desc = " " + _("(expires in 1 hour)")
else:
expiry = 0
expiry_desc = ""
@ -451,14 +463,14 @@ def auth():
{"otp": new_otp, "created_at": now, "expiry": expiry}
)
flash(
"New user link generated{}: {}auth/register?otp={}".format(
_("New user link generated{}: {}auth/register?otp={}").format(
expiry_desc, request.url_root, new_otp
),
"info",
)
else:
flash(
"Error: Only the admin account can issue new registration links.",
_("Error: Only the admin account can issue new registration links"),
"error",
)
elif action == "deleteuser":
@ -467,9 +479,11 @@ def auth():
if current_user.is_admin:
app.specter.delete_user(user)
users.remove(user)
flash("User {} was deleted successfully".format(user.username), "info")
flash(
_("User {} was deleted successfully").format(user.username), "info"
)
else:
flash("Error: Only the admin account can delete users", "error")
flash(_("Error: Only the admin account can delete users"), "error")
return render_template(
"settings/auth_settings.jinja",
method=method,
@ -489,7 +503,7 @@ def hwi():
if request.method == "POST":
hwi_bridge_url = request.form["hwi_bridge_url"]
app.specter.update_hwi_bridge_url(hwi_bridge_url, current_user)
flash("HWIBridge URL is updated! Don't forget to whitelist Specter!")
flash(_("HWIBridge URL is updated! Don't forget to whitelist Specter!"))
return render_template(
"settings/hwi_settings.jinja",
specter=app.specter,

View file

@ -10,6 +10,7 @@ from flask import (
jsonify,
flash,
)
from flask_babel import lazy_gettext as _
from flask_login import login_required, current_user
from flask import current_app as app
from mnemonic import Mnemonic
@ -120,10 +121,10 @@ def setup_tor():
t = threading.Thread(target=setup_tor_thread, args=(app.specter,))
t.start()
elif os.path.isfile(app.specter.torbrowser_path):
return {"error": "Tor is already installed"}
return {"error": _("Tor is already installed")}
elif app.specter.setup_status["torbrowser"]["stage_progress"] != -1:
return {"error": "Tor installation is still under progress"}
return {"success": "Starting Tor setup!"}
return {"error": _("Tor installation is still in progress")}
return {"success": _("Starting Tor setup!")}
@setup_endpoint.route("/setup_bitcoind", methods=["POST"])
@ -140,10 +141,10 @@ def setup_bitcoind():
)
t.start()
elif os.path.isfile(app.specter.bitcoind_path):
return {"error": "Bitcoin Core is already installed"}
return {"error": _("Bitcoin Core is already installed")}
elif app.specter.setup_status["bitcoind"]["stage_progress"] != -1:
return {"error": "Bitcoin Core installation is still under progress"}
return {"success": "Starting Bitcoin Core setup!"}
return {"error": _("Bitcoin Core installation is still in progress")}
return {"success": _("Starting Bitcoin Core setup!")}
@setup_endpoint.route("/setup_bitcoind_datadir", methods=["POST"])
@ -202,10 +203,10 @@ def setup_bitcoind_datadir():
)
t.start()
elif not os.path.isfile(app.specter.bitcoind_path):
return {"error": "Bitcoin Core in not installed but required for this step"}
return {"error": _("Bitcoin Core in not installed but required for this step")}
elif app.specter.setup_status["bitcoind"]["stage_progress"] != -1:
return {"error": "Bitcoin Core installation is still under progress"}
return {"success": "Starting Bitcoin Core setup!"}
return {"error": _("Bitcoin Core installation is still in progress")}
return {"success": _("Starting Bitcoin Core setup!")}
######################### Setup status (open endpoint) #######################################

View file

@ -17,6 +17,7 @@ import requests
from flask import Blueprint, Flask
from flask import current_app as app
from flask import flash, jsonify, redirect, render_template, request, url_for
from flask_babel import lazy_gettext as _
from flask_login import current_user, login_required
from werkzeug.wrappers import Response
from cryptoadvance.specter.util.psbt_creator import PsbtCreator
@ -47,7 +48,7 @@ wallets_endpoint = Blueprint("wallets_endpoint", __name__)
def handle_wallet_error(func_name, error):
flash(f"SpecterError while {func_name}: {error}", "error")
flash(_("SpecterError while {}: {}").format(func_name, error), "error")
app.logger.error(f"SpecterError while {func_name}: {error}")
app.specter.wallet_manager.update()
return redirect(url_for("about"))
@ -56,10 +57,9 @@ def handle_wallet_error(func_name, error):
def check_wallet(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""checks the wallet for healthyness A wrapper function"""
"""checks the wallet for healthiness A wrapper function"""
if kwargs["wallet_alias"]:
wallet_alias = kwargs["wallet_alias"]
print("--------------------checking wallet")
wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
wallet.get_info()
return func(*args, **kwargs)
@ -105,8 +105,7 @@ def failed_wallets():
delete_file(fullpath.replace(".json", "_txs.csv"))
app.specter.wallet_manager.update()
except Exception as e:
flash(f"Failed to delete failed wallet: {str(e)}", "error")
flash(_("Failed to delete wallet: {}").format(str(e)), "error")
return redirect("/")
@ -118,13 +117,15 @@ def failed_wallets():
def new_wallet_type():
err = None
if app.specter.chain is None:
err = "Configure Bitcoin Core to create wallets"
err = _("Configure Bitcoin Core to create wallets")
return render_template("base.jinja", error=err, specter=app.specter, rand=rand)
try:
# Make sure wallet is enabled on Bitcoin Core
app.specter.rpc.listwallets()
except Exception:
err = '<p><br>Configure Bitcoin Core is running with wallets disabled.<br><br>Please make sure disablewallet is off (set disablewallet=0 in your bitcoin.conf), then restart Bitcoin Core and try again.<br>See <a href="https://github.com/cryptoadvance/specter-desktop/blob/34ca139694ecafb2e7c2bd5ad5c4ac74c6d11501/docs/faq.md#im-not-sure-i-want-the-bitcoin-core-wallet-functionality-to-be-used-is-that-mandatory-if-so-is-it-considered-secure" target="_blank" style="color: white;">here</a> for more information.</p>'
err = _(
'<p><br>Bitcoin Core is running with wallets disabled.<br><br>Please make sure disablewallet is off (set disablewallet=0 in your bitcoin.conf), then restart Bitcoin Core and try again.<br>See <a href="https://github.com/cryptoadvance/specter-desktop/blob/34ca139694ecafb2e7c2bd5ad5c4ac74c6d11501/docs/faq.md#im-not-sure-i-want-the-bitcoin-core-wallet-functionality-to-be-used-is-that-mandatory-if-so-is-it-considered-secure" target="_blank" style="color: white;">here</a> for more information.</p>'
)
return render_template("base.jinja", error=err, specter=app.specter, rand=rand)
return render_template(
"wallet/new_wallet/new_wallet_type.jinja", specter=app.specter, rand=rand
@ -136,7 +137,7 @@ def new_wallet_type():
def new_wallet(wallet_type):
wallet_types = ["simple", "multisig", "import_wallet"]
if wallet_type not in wallet_types:
flash("Unknown wallet type requested", "error")
flash(_("Unknown wallet type requested"), "error")
return redirect(url_for("wallets_endpoint.new_wallet_type"))
err = None
@ -163,7 +164,7 @@ def new_wallet(wallet_type):
except SpecterError as se:
flash(str(se), "error")
return redirect(url_for("wallets_endpoint.new_wallet_type"))
flash("Wallet imported successfully", "info")
flash(_("Wallet imported successfully"), "info")
try:
wallet_importer.rescan_as_needed(app.specter)
except SpecterError as se:
@ -199,17 +200,18 @@ def new_wallet(wallet_type):
return render_template(
"wallet/new_wallet/new_wallet.jinja",
wallet_type=wallet_type,
error="No device was selected. Please select a device to create the wallet for.",
error=_(
"No device was selected. Please select a device to create the wallet for."
),
specter=app.specter,
rand=rand,
)
devices = get_devices_with_keys_by_type(app, cosigners, wallet_type)
for device in devices:
if len(device.keys) == 0:
err = (
"Device %s doesn't have keys matching this wallet type"
% device.name
)
err = _(
"Device {} doesn't have keys matching this wallet type"
).format(device.name)
break
name = wallet_type.title()
@ -235,7 +237,7 @@ def new_wallet(wallet_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"
err = _("Wallet already exists")
if err:
devices = [
app.specter.device_manager.get_by_alias(
@ -270,7 +272,9 @@ def new_wallet(wallet_type):
except:
pass
if len(keys) != sigs_total or len(cosigners) != sigs_total:
err = "No keys were selected for device, please try adding keys first"
err = _(
"No keys were selected for device, please try adding keys first"
)
devices = [
app.specter.device_manager.get_by_alias(
request.form.get("cosigner{}".format(i))
@ -294,7 +298,7 @@ def new_wallet(wallet_type):
wallet_name, sigs_required, address_type, keys, cosigners
)
except Exception as e:
err = f"Failed to create wallet. Error: {e}"
err = _("Failed to create wallet. Error: {}").format(e)
return render_template(
"wallet/new_wallet/new_wallet_keys.jinja",
cosigners=cosigners,
@ -483,7 +487,9 @@ def send_new(wallet_alias):
selected_coins = request.form.getlist("coinselect")
fee_estimation_data = get_fees(app.specter, app.config)
if fee_estimation_data.get("failed", None):
flash("Failed to fetch fee estimations, please use the manual fee calculation")
flash(
_("Failed to fetch fee estimations. Please use the manual fee calculation.")
)
fee_rate = fee_estimation_data["hourFee"]
@ -535,7 +541,7 @@ def send_new(wallet_alias):
rand=rand,
)
except Exception as e:
flash("Failed to perform RBF. Error: %s" % e, "error")
flash(_("Failed to perform RBF. Error: {}").format(e), "error")
elif action == "rbf_cancel":
try:
rbf_tx_id = request.form["rbf_tx_id"]
@ -551,7 +557,10 @@ def send_new(wallet_alias):
rand=rand,
)
except Exception as e:
flash("Failed to cancel transaction with RBF. Error: %s" % e, "error")
flash(
_("Failed to cancel transaction with RBF. Error: {}").format(e),
"error",
)
elif action == "rbf_edit":
try:
decoded_tx = wallet.decode_tx(rbf_tx_id)
@ -565,7 +574,7 @@ def send_new(wallet_alias):
fee_options = "manual"
rbf = True
except Exception as e:
flash("Failed to perform RBF. Error: %s" % e, "error")
flash(_("Failed to perform RBF. Error: {}").format(e), "error")
elif action == "signhotwallet":
passphrase = request.form["passphrase"]
psbt = json.loads(request.form["psbt"])
@ -588,10 +597,10 @@ def send_new(wallet_alias):
signed_psbt = signed_psbt["psbt"]
except Exception as e:
signed_psbt = None
flash("Failed to sign PSBT: %s" % e, "error")
flash(_("Failed to sign PSBT: {}").format(e), "error")
else:
signed_psbt = None
flash("Device already signed the PSBT", "error")
flash(_("Device already signed the PSBT"), "error")
return render_template(
"wallet/send/sign/wallet_send_sign_psbt.jinja",
signed_psbt=signed_psbt,
@ -607,7 +616,7 @@ def send_new(wallet_alias):
try:
rbf_utxo = wallet.get_rbf_utxo(rbf_tx_id)
except Exception as e:
flash("Failed to get RBF coins. Error: %s" % e, "error")
flash(_("Failed to get RBF coins. Error: {}").format(e), "error")
show_advanced_settings = (
ui_option != "ui"
@ -661,7 +670,7 @@ def send_pending(wallet_alias):
)
except Exception as e:
app.logger.error("Could not delete Pending PSBT: %s" % e)
flash("Could not delete Pending PSBT!", "error")
flash(_("Could not delete Pending PSBT!"), "error")
elif action == "openpsbt":
psbt = json.loads(request.form["pending_psbt"])
return render_template(
@ -699,7 +708,7 @@ def import_psbt(wallet_alias):
b64psbt = "".join(request.form["rawpsbt"].split())
psbt = wallet.importpsbt(b64psbt)
except Exception as e:
flash("Could not import PSBT: %s" % e, "error")
flash(_("Could not import PSBT: {}").format(e), "error")
return redirect(
url_for("wallets_endpoint.import_psbt", wallet_alias=wallet_alias)
)
@ -773,7 +782,7 @@ def settings(wallet_alias):
elif action == "abortrescan":
res = wallet.rpc.abortrescan()
if not res:
error = "Failed to abort rescan. Maybe already complete?"
error = _("Failed to abort rescan. Maybe already complete?")
wallet.getdata()
elif action == "rescanutxo":
explorer = None
@ -810,11 +819,11 @@ def settings(wallet_alias):
elif action == "rename":
wallet_name = request.form["newtitle"]
if not wallet_name:
flash("Wallet name cannot be empty", "error")
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")
flash(_("Wallet already exists"), "error")
else:
app.specter.wallet_manager.rename_wallet(wallet, wallet_name)
@ -846,7 +855,7 @@ def combine(wallet_alias):
for i, psbt in enumerate(psbts):
if not psbt:
return "Cannot parse empty data as PSBT", 500
return _("Cannot parse empty data as PSBT"), 500
if "UR:BYTES/" in psbt.upper():
psbt = bcur2base64(psbt).decode()
@ -870,7 +879,7 @@ def combine(wallet_alias):
try:
bytes.fromhex(psbt)
except:
return "Invalid transaction format", 500
return _("Invalid transaction format"), 500
try:
if "hex" in raw:
@ -886,84 +895,84 @@ def combine(wallet_alias):
except RpcError as e:
return e.error_msg, e.status_code
except Exception as e:
return "Unknown error: %r" % e, 500
return _("Unknown error: {}").format(e), 500
return json.dumps(raw)
@wallets_endpoint.route("/wallet/<wallet_alias>/broadcast/", methods=["GET", "POST"])
@wallets_endpoint.route("/wallet/<wallet_alias>/broadcast/", methods=["POST"])
@login_required
def broadcast(wallet_alias):
wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
if request.method == "POST":
tx = request.form.get("tx")
res = wallet.rpc.testmempoolaccept([tx])[0]
if res["allowed"]:
app.specter.broadcast(tx)
wallet.delete_pending_psbt(get_txid(tx))
return jsonify(success=True)
else:
return jsonify(
success=False,
error="Failed to broadcast transaction: transaction is invalid\n%s"
% res["reject-reason"],
)
return jsonify(success=False, error="broadcast tx request must use POST")
tx = request.form.get("tx")
res = wallet.rpc.testmempoolaccept([tx])[0]
if res["allowed"]:
app.specter.broadcast(tx)
wallet.delete_pending_psbt(get_txid(tx))
return jsonify(success=True)
else:
return jsonify(
success=False,
error=_(
"Failed to broadcast transaction: transaction is invalid\n{}"
).format(res["reject-reason"]),
)
@wallets_endpoint.route(
"/wallet/<wallet_alias>/broadcast_blockexplorer/", methods=["GET", "POST"]
"/wallet/<wallet_alias>/broadcast_blockexplorer/", methods=["POST"]
)
@login_required
def broadcast_blockexplorer(wallet_alias):
wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
if request.method == "POST":
tx = request.form.get("tx")
explorer = request.form.get("explorer")
use_tor = request.form.get("use_tor", "true") == "true"
res = wallet.rpc.testmempoolaccept([tx])[0]
if res["allowed"]:
try:
if app.specter.chain == "main":
url_network = ""
elif app.specter.chain == "liquidv1":
url_network = "liquid/"
elif app.specter.chain == "test" or app.specter.chain == "testnet":
url_network = "testnet/"
elif app.specter.chain == "signet":
url_network = "signet/"
else:
return jsonify(
success=False,
error=f"Failed to broadcast transaction. Network not supported.",
)
if explorer == "mempool":
explorer = f"MEMPOOL_SPACE{'_ONION' if use_tor else ''}"
elif explorer == "blockstream":
explorer = f"BLOCKSTREAM_INFO{'_ONION' if use_tor else ''}"
else:
return jsonify(
success=False,
error=f"Failed to broadcast transaction. Block explorer not supported.",
)
requests_session = app.specter.requests_session(force_tor=use_tor)
requests_session.post(
f"{app.config['EXPLORERS_LIST'][explorer]['url']}{url_network}api/tx",
data=tx,
)
wallet.delete_pending_psbt(get_txid(tx))
return jsonify(success=True)
except Exception as e:
tx = request.form.get("tx")
explorer = request.form.get("explorer")
use_tor = request.form.get("use_tor", "true") == "true"
res = wallet.rpc.testmempoolaccept([tx])[0]
if res["allowed"]:
try:
if app.specter.chain == "main":
url_network = ""
elif app.specter.chain == "liquidv1":
url_network = "liquid/"
elif app.specter.chain == "test" or app.specter.chain == "testnet":
url_network = "testnet/"
elif app.specter.chain == "signet":
url_network = "signet/"
else:
return jsonify(
success=False,
error=f"Failed to broadcast transaction with error: {e}",
error=_("Failed to broadcast transaction. Network not supported."),
)
else:
if explorer == "mempool":
explorer = f"MEMPOOL_SPACE{'_ONION' if use_tor else ''}"
elif explorer == "blockstream":
explorer = f"BLOCKSTREAM_INFO{'_ONION' if use_tor else ''}"
else:
return jsonify(
success=False,
error=_(
"Failed to broadcast transaction. Block explorer not supported."
),
)
requests_session = app.specter.requests_session(force_tor=use_tor)
requests_session.post(
f"{app.config['EXPLORERS_LIST'][explorer]['url']}{url_network}api/tx",
data=tx,
)
wallet.delete_pending_psbt(get_txid(tx))
return jsonify(success=True)
except Exception as e:
return jsonify(
success=False,
error="Failed to broadcast transaction: transaction is invalid\n%s"
% res["reject-reason"],
error=_("Failed to broadcast transaction with error: {}").format(e),
)
return jsonify(success=False, error="broadcast tx request must use POST")
else:
return jsonify(
success=False,
error=_(
"Failed to broadcast transaction: transaction is invalid\n{}"
).format(res["reject-reason"]),
)
@wallets_endpoint.route("/wallet/<wallet_alias>/decoderawtx/", methods=["GET", "POST"])
@ -1010,15 +1019,15 @@ def decoderawtx(wallet_alias):
if "asset" in v:
v["assetlabel"] = app.specter.asset_label(v["asset"])
return {
"success": True,
"tx": tx,
"rawtx": rawtx,
"walletName": wallet.name,
}
return jsonify(
success=True,
tx=tx,
rawtx=rawtx,
walletName=wallet.name,
)
except Exception as e:
app.logger.warning("Failed to fetch transaction data. Exception: {}".format(e))
return {"success": False}
return jsonify(success=False)
@wallets_endpoint.route(
@ -1030,10 +1039,10 @@ def rescan_progress(wallet_alias):
try:
wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
wallet.get_info()
return {
"active": wallet.rescan_progress is not None,
"progress": wallet.rescan_progress,
}
return jsonify(
active=wallet.rescan_progress is not None,
progress=wallet.rescan_progress,
)
except SpecterError as se:
app.logger.error("SpecterError while get wallet rescan_progress: %s" % se)
return {}
@ -1046,16 +1055,16 @@ def get_label(wallet_alias):
wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
address = request.form.get("address", "")
label = wallet.getlabel(address)
return {
"address": address,
"label": label,
}
return jsonify(
address=address,
label=label,
)
except Exception as e:
handle_exception(e)
return {
"success": False,
"error": f"Exception trying to get address label: Error: {e}",
}
return jsonify(
success=False,
error=_("Exception trying to get address label: Error: {}").format(e),
)
@wallets_endpoint.route("/wallet/<wallet_alias>/set_label", methods=["POST"])
@ -1066,7 +1075,7 @@ def set_label(wallet_alias):
address = request.form["address"]
label = request.form["label"].rstrip()
wallet.setlabel(address, label)
return {"success": True}
return jsonify(success=True)
@wallets_endpoint.route("/wallet/<wallet_alias>/txlist", methods=["POST"])
@ -1174,10 +1183,10 @@ def addresses_list(wallet_alias):
addresses_list, idx=idx, limit=limit, sortby=sortby, sortdir=sortdir
)
return {
"addressesList": json.dumps(result["addressesList"]),
"pageCount": result["pageCount"],
}
return jsonify(
addressesList=json.dumps(result["addressesList"]),
pageCount=result["pageCount"],
)
@wallets_endpoint.route("/wallet/<wallet_alias>/addressinfo/", methods=["POST"])
@ -1199,9 +1208,8 @@ def addressinfo(wallet_alias):
**address_info,
}
except Exception as e:
app.logger.warning("Failed to fetch address data. Exception: {}".format(e))
return {"success": False}
return jsonify(success=False)
################## Wallet CSV export data endpoints #######################
@ -1263,7 +1271,7 @@ def addresses_list_csv(wallet_alias):
return response
except Exception as e:
app.logger.error("Failed to export addresses list. Error: %s" % e)
flash("Failed to export addresses list. Error: %s" % e, "error")
flash(_("Failed to export addresses list. Error: {}").format(e), "error")
return redirect(url_for("index"))
@ -1332,7 +1340,7 @@ def utxo_csv(wallet_alias):
return response
except Exception as e:
logging.exception(e)
return "Failed to export wallet utxo. Error: %s" % e, 500
return _("Failed to export wallet utxo. Error: {}").format(e), 500
# Export all wallets transaction history combined
@ -1367,7 +1375,7 @@ def wallet_overview_txs_csv():
return response
except Exception as e:
logging.exception(e)
return "Failed to export wallets overview history. Error: %s" % e, 500
return _("Failed to export wallets overview history. Error: {}").format(e), 500
# Export all wallets transaction history combined
@ -1399,7 +1407,7 @@ def wallet_overview_utxo_csv():
return response
except Exception as e:
logging.exception(e)
return "Failed to export wallets overview utxo. Error: %s" % e, 500
return _("Failed to export wallets overview utxo. Error: {}").format(e), 500
################## Helpers #######################
@ -1425,22 +1433,22 @@ def txlist_to_csv(wallet, _txlist, specter, current_user, includePricesHistory=F
elif specter.price_provider.endswith("_gbp"):
symbol = "GBP"
row = (
"Date",
"Label",
"Category",
"Amount ({})".format(specter.unit.upper()),
"Value ({})".format(symbol),
"Rate (BTC/{})".format(symbol)
_("Date"),
_("Label"),
_("Category"),
_("Amount ({})").format(specter.unit.upper()),
_("Value ({})").format(symbol),
_("Rate (BTC/{})").format(symbol)
if specter.unit != "sat"
else "Rate ({}/SAT)".format(symbol),
"TxID",
"Address",
"Block Height",
"Timestamp",
"Raw Transaction",
else _("Rate ({}/SAT)").format(symbol),
_("TxID"),
_("Address"),
_("Block Height"),
_("Timestamp"),
_("Raw Transaction"),
)
if not wallet:
row = ("Wallet",) + row
row = (_("Wallet"),) + row
w.writerow(row)
yield data.getvalue()
data.seek(0)
@ -1515,11 +1523,11 @@ def addresses_list_to_csv(wallet):
w = csv.writer(data)
# write header
row = (
"Address",
"Label",
"Index",
"Used",
"Current balance",
_("Address"),
_("Label"),
_("Index"),
_("Used"),
_("Current balance"),
)
w.writerow(row)
yield data.getvalue()
@ -1534,15 +1542,16 @@ def addresses_list_to_csv(wallet):
row = (
address,
address_info.label,
"(external)"
_("(external)")
if address_info.is_external
else (
str(address_info.index) + (" (change)" if address_info.change else "")
str(address_info.index)
+ (_(" (change)") if address_info.change else "")
),
address_info.used,
)
if address_info.is_external:
balance_on_address = "unknown (external address)"
balance_on_address = _("unknown (external address)")
else:
balance_on_address = 0
if address_info.used:
@ -1565,13 +1574,13 @@ def wallet_addresses_list_to_csv(addresses_list):
w = csv.writer(data)
# write header
row = (
"Index",
"Address",
"Type",
"Label",
"Used",
"UTXO",
"Amount (BTC)",
_("Index"),
_("Address"),
_("Type"),
_("Label"),
_("Used"),
_("UTXO"),
_("Amount (BTC)"),
)
w.writerow(row)
yield data.getvalue()

View file

@ -2,7 +2,7 @@
<html>
<head>
<link rel="shortcut icon" type="image/png" href="{{ url_for('static', filename='img/icon.png') }}"/>
<title>Specter Desktop</title>
<title>{{ _("Specter Desktop") }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
<style type="text/css">
@ -15,15 +15,15 @@
<main>
<br><br><br>
<div class="card" style="width:90%; max-width: 800px;">
<h1>Server error!</h1>
Exception: <b>{{ error }}</b><br><br>
<h1>{{ _("Server error!") }}</h1>
{{ _("Exception") }}: <b>{{ error }}</b><br><br>
<div><br>
<a class="btn" href="{{ url_for('index') }}">Back to the main page</a>
<a class="btn" href="{{ url_for('index') }}">{{ _("Back to the main page") }}</a>
<br></div>
Details:
{{ _("Details") }}:
<pre><code>{{ traceback }}</code></pre>
<br>
<div>Please let us know! Create an <a href="https://github.com/cryptoadvance/specter-desktop/issues" target="_blank">issue</a> or ping us on <a href="https://t.me/spectersupport" target="_blank">Telegram</a> to fix it.</div>
<div>{{ _('Please let us know! Create an <a href="https://github.com/cryptoadvance/specter-desktop/issues" target="_blank">issue</a> or ping us on <a href="https://t.me/spectersupport" target="_blank">Telegram</a> to fix it.') }}</div>
</div>
</main>
</body>

View file

@ -2,7 +2,7 @@
<html>
<head>
<link rel="shortcut icon" type="image/png" href="{{ url_for('static', filename='img/icon.png') }}"/>
<title>Specter Desktop</title>
<title>{{ _("Specter Desktop") }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
<style type="text/css">
@ -15,20 +15,20 @@
<main>
<br><br><br>
<div class="card" style="width:90%; max-width: 800px;">
<h1>Timeout Error!</h1>
Exception: <b>{{ error }}</b><br><br>
<h1>{{ _("Timeout Error!") }}</h1>
{{ _("Exception") }}: <b>{{ error }}</b><br><br>
<div><br>
<a class="btn" href="{{ url_for('index') }}">Back to the main page</a>
<a class="btn" href="{{ url_for('index') }}">{{ _("Back to the main page") }}</a>
<br></div>
Please have a look in the below log. There are effectively two ways to resolve this:
{{ _("Please have a look in the below log. There are effectively two ways to resolve this:") }}"
<ul>
<li>Either the process was just slow and specter too impatient</li>
<li>The process is in trouble which should be reflexted by the content of the logs below</li>
<li>{{ _("Either the process was just slow and Specter was too impatient") }}</li>
<li>{{ _("The process is in trouble which should be reflected by the content of the logs below") }}</li>
</ul>
Details from the Log-File:
{{ _("Details from the log file") }}:
<pre><code>{{ loglines }}</code></pre>
<br>
<div>Please let us know! Create an <a href="https://github.com/cryptoadvance/specter-desktop/issues" target="_blank">issue</a> or ping us on <a href="https://t.me/spectersupport" target="_blank">Telegram</a> to fix it.</div>
<div>{{ _('Please let us know! Create an <a href="https://github.com/cryptoadvance/specter-desktop/issues" target="_blank">issue</a> or ping us on <a href="https://t.me/spectersupport" target="_blank">Telegram</a> to fix it.') }}</div>
</div>
</main>
</body>

View file

@ -2,7 +2,7 @@
<html>
<head>
<link rel="shortcut icon" type="image/png" href="{{ url_for('static', filename='img/icon.png') }}"/>
<title>Specter Desktop</title>
<title>{{ _("Specter Desktop") }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="format-detection" content="telephone=no">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}?{{rand}}">
@ -60,158 +60,164 @@
{% endif %}
{% endwith %}
{% if error %}
<message-box type="error">ERROR: {{error}}</message-box>
<message-box type="error">{{ _("ERROR:") }} {{error}}</message-box>
{% endif %}
</div>
{% block main %}
{% if error %}
<br><br>Something went wrong :(<br><br>{{error}}
<br><br>{{ _("Something went wrong:") }} (<br><br>{{error}}
{% else %}
<br>
<div style="max-width: 60%; text-align: center;">
<h1 style="font-size: 1.8em;">
<img src="{{ url_for('static', filename='img/icon.png') }}" width="40px" style="vertical-align: top; margin-right: 20px;"/>
Welcome to Specter Desktop
{{ _('Welcome to Specter Desktop') }}
<img src="{{ url_for('static', filename='img/ghost.png') }}" width="40px" style="vertical-align: top; margin-left: 20px;"/>
</h1>
<p style="font-size: 1.2em; font-weight: bolder;">Specter Desktop is a convenient wallet interface to better use your Bitcoin Core node. Its good for singlesig wallets, but comes with a special focus on multisignature setups for different hardware wallets and air-gapped signing devices.</p>
<br><br>
<p style="font-size: 1.2em; font-weight: bolder;">{{ _("Specter Desktop is a convenient wallet interface to better use your Bitcoin Core node. Its good for singlesig wallets, but comes with a special focus on multisignature setups for different hardware wallets and air-gapped signing devices.") }}</p>
<div style="max-width: 15em; text-align: center; margin-bottom: 3em; margin-left: auto; margin-right: auto;">
{% include "includes/language/language_select.jinja" %}
</div>
{% if not specter.chain %}
<a href="{{url_for('setup_endpoint.' + specter.setup_status.stage)}}" class="btn action centered" style="max-width: 200px;">{% if specter.setup_status.stage == "start" %}Get started!{% else %}Continue setup{% endif %}</a>
<br><br>
<a href="{{url_for('setup_endpoint.' + specter.setup_status.stage)}}" class="btn action centered" style="max-width: 200px;">{% if specter.setup_status.stage == "start" %}{{ _('Get started!') }}{% else %}{{ _('Continue setup') }}{% endif %}</a>
<br><br>
{% else %}
<h2 style="font-size: 1.6em;">Getting Started</h2><br>
{% if ('://localhost:' not in url_for('index', _external=True) and '://127.0.0.1:' not in url_for('index', _external=True)) and specter.hwi_bridge_url != "http://127.0.0.1:25441/hwi/api/" %}
<div style="margin: auto; display: block;">
<div class="card" style="margin: auto;padding: 5px 10px 15px; width: 90%;">
<div class="row">
<img src="{{ url_for('static', filename='img/warning_sign.svg') }}" style="width: 20px;"/>
<p style="color: #FF9A00; font-size: 1.1em; margin-left: 10px;">Using Specter from a remote machine?</p>
<h2 style="font-size: 1.6em;">{{ _('Getting Started') }}</h2><br>
{% if ('://localhost:' not in url_for('index', _external=True) and '://127.0.0.1:' not in url_for('index', _external=True)) and specter.hwi_bridge_url != "http://127.0.0.1:25441/hwi/api/" %}
<div style="margin: auto; display: block;">
<div class="card" style="margin: auto;padding: 5px 10px 15px; width: 90%;">
<div class="row">
<img src="{{ url_for('static', filename='img/warning_sign.svg') }}" style="width: 20px;"/>
<p style="color: #FF9A00; font-size: 1.1em; margin-left: 10px;">{{ _('Using Specter from a remote machine?') }}</p>
</div>
<p style="margin-left: 30px; margin-top: -10px; margin-right: 20px; text-align: left;">{{ _('If you run Specter on a remote machine, like Umbrel or a myNode, you need to update settings to connect hardware wallets via USB.') }}</p>
<a href="{{ url_for('settings_endpoint.hwi') }}" style="color: #fff; text-decoration: underline; cursor: pointer;">
<h4>{{ _("Update your settings") }}</h4>
</a>
</div>
<p style="margin-left: 30px; margin-top: -10px; margin-right: 20px; text-align: left;">If you run Specter on a remote machine, like Umbrel or a myNode, you need to update settings to connect hardware wallets via USB.</p>
<a href="{{ url_for('settings_endpoint.hwi') }}" style="color: #fff; text-decoration: underline; cursor: pointer;">
<h4>Update your settings</h4>
</a>
<br>
</div>
<br>
</div>
{% endif %}
<table style="width: 90%; margin: auto;">
<tbody>
<tr>
<td>
<img src="{{ url_for('static', filename='img/checkbox-' + ('un' if not specter.chain else '') + 'tick.svg') }}" width="30px">
</td>
<td style="text-align: left;">
<a href="{{ url_for('setup_endpoint.node_type') }}">
<h2>Connect Specter with Bitcoin Core node.</h2>
</a>
</td>
<td style="text-align: left;">
{% if not specter.chain %}
<button type="button" class="btn" onclick="showPageOverlay('connect-node-help-popup')">Need help?</button>
<div id="connect-node-help-popup" class="hidden">
<h1>Connecting Specter to your Bitcoin Core node</h1>
<div class="overflow">
<button style="padding: 10px; width: 80%; font-size: 1.2em; margin: 20px auto;" type="button" class="btn" onclick="hidePageOverlay();showPageOverlay('no-node-help-popup')">I don't have a Bitcoin Core node yet.</button>
<button style="padding: 10px; width: 80%; font-size: 1.2em; margin: 20px auto;" type="button" class="btn" onclick="hidePageOverlay();showPageOverlay('local-node-help-popup')">I have Bitcoin Core on this computer.</button>
<button style="padding: 10px; width: 80%; font-size: 1.2em; margin: 20px auto;" type="button" class="btn" onclick="hidePageOverlay();showPageOverlay('remote-node-help-popup')">I have a remote Bitcoin Core node.</button>
{% endif %}
<table style="width: 90%; margin: auto;">
<tbody>
<tr>
<td>
<img src="{{ url_for('static', filename='img/checkbox-' + ('un' if not specter.chain else '') + 'tick.svg') }}" width="30px">
</td>
<td style="text-align: left;">
<a href="{{ url_for('setup_endpoint.node_type') }}">
<h2>{{ _("Connect Specter with Bitcoin Core node.") }}</h2>
</a>
</td>
<td style="text-align: left;">
{% if not specter.chain %}
<button type="button" class="btn" onclick="showPageOverlay('connect-node-help-popup')">Need help?</button>
<div id="connect-node-help-popup" class="hidden">
<h1>{{ _("Connecting Specter to your Bitcoin Core node") }}</h1>
<div class="overflow">
<button style="padding: 10px; width: 80%; font-size: 1.2em; margin: 20px auto;" type="button" class="btn" onclick="hidePageOverlay();showPageOverlay('no-node-help-popup')">{{ _("I don't have a Bitcoin Core node yet.") }}</button>
<button style="padding: 10px; width: 80%; font-size: 1.2em; margin: 20px auto;" type="button" class="btn" onclick="hidePageOverlay();showPageOverlay('local-node-help-popup')">{{ _("I have Bitcoin Core on this computer.") }}</button>
<button style="padding: 10px; width: 80%; font-size: 1.2em; margin: 20px auto;" type="button" class="btn" onclick="hidePageOverlay();showPageOverlay('remote-node-help-popup')">{{ _("I have a remote Bitcoin Core node.") }}</button>
</div>
</div>
</div>
<div id="no-node-help-popup" class="hidden">
<h1>Installing and Setting Up Bitcoin Core</h1>
Check out these video tutorials on downloading and setting up Bitcoin Core with Specter Desktop:<br><br>
<ul style="text-align: left; margin: auto; line-height: 2;">
<li><a href="https://www.keepitsimplebitcoin.com/setup-bitcoin-core/" target="_blank" style="color: #fff;">KIS: Setting up Bitcoin Core</a></li>
<li><a href="https://www.keepitsimplebitcoin.com/how-to-use-specter-wallet-1/" target="_blank" style="color: #fff;">KIS: Setting up Specter Desktop</a></li>
<li><a href="https://www.youtube.com/watch?v=4koKF2MDXtk&t=2s" target="_blank" style="color: #fff;">Ministry of Nodes: Setting up Bitcoin Core and Specter on Windows</a></li>
</ul>
</div>
<div id="local-node-help-popup" class="hidden">
<h1>Connecting Specter to your local Bitcoin Core node</h1>
<p>If you're running a local Bitcoin Core node, Specter should be able to auto-detect it if its data folder is located at the default location.<br>
In case it isn't, you might need to specify the Bitcoin Core data folder path in Specter's settings page.<br>
Alternatively, you can switch off auto-detect from the settings and provide the Bitcoin Core RPC credentials and host URL manually.<br><br>
<b>Please note:</b><br>Your node must be configured with <code>server=1</code> line in the <code>bitcoin.conf</code> file.<br>
If you don't have that, add it and restart Bitcoin Core.
</p>
<br><br>
In addition, you can check out these video tutorials for setting up Bitcoin Core with Specter Desktop:<br><br>
<ul style="text-align: left; margin: auto; line-height: 2;">
<li><a href="https://www.keepitsimplebitcoin.com/setup-bitcoin-core/" target="_blank" style="color: #fff;">KIS: Setting up Bitcoin Core</a></li>
<li><a href="https://www.keepitsimplebitcoin.com/how-to-use-specter-wallet-1/" target="_blank" style="color: #fff;">KIS: Setting up Specter Desktop</a></li>
<li><a href="https://www.youtube.com/watch?v=4koKF2MDXtk&t=2s" target="_blank" style="color: #fff;">Ministry of Nodes: Setting up Bitcoin Core and Specter on Windows</a></li>
</ul>
</div>
<div id="remote-node-help-popup" class="hidden">
<h1>Connecting Specter to your remote Bitcoin Core node</h1>
<h3>Connecting to a "node in a box"</h3>
<p style="font-size: 1.05em;">
Specter can be used with a remote Bitcoin Core node running on a machine like RaspiBlitz, myNode, Nodl, Umbrel, and Embassy.<br>
RaspiBlitz and myNode currently allow running Specter in the remote machine directly, which means it's possible to use it and skip setting up connection manually.<br><br>
If you use a different node or would still like to connect run Specter locally but connect remotely, you can do so by going to Settings, Bitcoin Core tab, and provide the Bitcoin Core RPC credentials and host URL manually.
</p>
<h3>Connecting over Tor</h3>
<p style="font-size: 1.05em;">Connecting over Tor requires that you first configure Tor on your local machine.<br>
You can checkout <a href="https://www.keepitsimplebitcoin.com/how-to-install-tor/" style="color: #fff;" target="_blank">this video tutorial</a>, or read the documentation for <a href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/tor.md" target="_blank" style="color: #fff;">Mac and Linux</a> or <a href="https://github.com/Fonta1n3/FullyNoded/blob/master/Docs/Tor/Tor.md#windows-10" target="_blank" style="color: #fff;">Window</a><br>
</p>
</div>
{% endif %}
</td>
</tr>
<tr>
<td>
<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_type') }}">
<h2>Add signing devices you wish to use.</h2>
</a>
</td>
<td style="text-align: left;">
</td>
</tr>
<tr>
<td>
<img src="{{ url_for('static', filename='img/checkbox-' + ('un' if not (specter.wallet_manager.wallets | length) else '') + 'tick.svg') }}" width="30px">
</td>
<td style="text-align: left;">
<a href="{{ url_for('wallets_endpoint.new_wallet_type') }}">
<h2>Create your first wallet!</h2>
</a>
</td>
<td style="text-align: left;">
</td>
</tr>
{% if specter.chain == 'main' %}
<tr>
<td>
<img src="{{ url_for('static', filename='img/ghost.png')}}" width="30px">
</td>
<td style="text-align: left;">
<a href="{{ url_for('get_whitepaper') }}">
<h2>And when you are all set: Get the whitepaper from the timechain.</h2>
</a>
</td>
<td style="text-align: left;">
</td>
</tr>
{% endif %}
</tbody>
</table>
<br>
<div id="no-node-help-popup" class="hidden">
<h1>{{ _("Installing and Setting Up Bitcoin Core") }}</h1>
{{ _("Check out these video tutorials on downloading and setting up Bitcoin Core with Specter Desktop") }}:<br><br>
<ul style="text-align: left; margin: auto; line-height: 2;">
<li><a href="https://www.keepitsimplebitcoin.com/setup-bitcoin-core/" target="_blank" style="color: #fff;">KIS: Setting up Bitcoin Core</a></li>
<li><a href="https://www.keepitsimplebitcoin.com/how-to-use-specter-wallet-1/" target="_blank" style="color: #fff;">KIS: Setting up Specter Desktop</a></li>
<li><a href="https://www.youtube.com/watch?v=4koKF2MDXtk&t=2s" target="_blank" style="color: #fff;">Ministry of Nodes: Setting up Bitcoin Core and Specter on Windows</a></li>
</ul>
</div>
<div id="local-node-help-popup" class="hidden">
<h1>{{ _('Connecting Specter to your local Bitcoin Core node') }}</h1>
<p>{{ _("If you're running a local Bitcoin Core node, Specter should be able to auto-detect it if its data folder is located at the default location.") }}<br>
{{ _("In case it isn't, you might need to specify the Bitcoin Core data folder path in Specter's settings page.") }}<br>
{{ _("Alternatively, you can switch off auto-detect from the settings and provide the Bitcoin Core RPC credentials and host URL manually.") }}<br><br>
{{ _("<b>Please note:</b><br>Your node must be configured with <code>server=1</code> line in the <code>bitcoin.conf</code> file.") }}<br>
{{ _("If you don't have that, add it and restart Bitcoin Core.") }}"
</p>
<br><br>
{{ _("In addition, you can check out these video tutorials for setting up Bitcoin Core with Specter Desktop") }}:<br><br>
<ul style="text-align: left; margin: auto; line-height: 2;">
<li><a href="https://www.keepitsimplebitcoin.com/setup-bitcoin-core/" target="_blank" style="color: #fff;">KIS: Setting up Bitcoin Core</a></li>
<li><a href="https://www.keepitsimplebitcoin.com/how-to-use-specter-wallet-1/" target="_blank" style="color: #fff;">KIS: Setting up Specter Desktop</a></li>
<li><a href="https://www.youtube.com/watch?v=4koKF2MDXtk&t=2s" target="_blank" style="color: #fff;">Ministry of Nodes: Setting up Bitcoin Core and Specter on Windows</a></li>
</ul>
</div>
<div id="remote-node-help-popup" class="hidden">
<h1>{{ _("Connecting Specter to your remote Bitcoin Core node") }}</h1>
<h3>{{ _('Connecting to a "node in a box"') }}</h3>
<p style="font-size: 1.05em;">
{{ _("Specter can be used with a remote Bitcoin Core node running on a machine like RaspiBlitz, myNode, Nodl, Umbrel, and Embassy.") }}<br>
{{ _("RaspiBlitz and myNode currently allow running Specter in the remote machine directly, which means it's possible to use it and skip setting up connection manually.") }}<br><br>
{{ _("If you use a different node or would still like to connect run Specter locally but connect remotely, you can do so by going to Settings, Bitcoin Core tab, and provide the Bitcoin Core RPC credentials and host URL manually.") }}"
</p>
<h3>{{ _("Connecting over Tor") }}</h3>
<p style="font-size: 1.05em;">{{ _("Connecting over Tor requires that you first configure Tor on your local machine.") }}<br>
You can checkout <a href="https://www.keepitsimplebitcoin.com/how-to-install-tor/" style="color: #fff;" target="_blank">this video tutorial</a>, or read the documentation for <a href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/tor.md" target="_blank" style="color: #fff;">Mac and Linux</a> or <a href="https://github.com/Fonta1n3/FullyNoded/blob/master/Docs/Tor/Tor.md#windows-10" target="_blank" style="color: #fff;">Window</a><br>
</p>
</div>
{% endif %}
</td>
</tr>
<tr>
<td>
<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_type') }}">
<h2>{{ _("Add signing devices you wish to use.") }}</h2>
</a>
</td>
<td style="text-align: left;">
</td>
</tr>
<tr>
<td>
<img src="{{ url_for('static', filename='img/checkbox-' + ('un' if not (specter.wallet_manager.wallets | length) else '') + 'tick.svg') }}" width="30px">
</td>
<td style="text-align: left;">
<a href="{{ url_for('wallets_endpoint.new_wallet_type') }}">
<h2>{{ _("Create your first wallet!") }}</h2>
</a>
</td>
<td style="text-align: left;">
</td>
</tr>
{% if specter.chain == 'main' %}
<tr>
<td>
<img src="{{ url_for('static', filename='img/ghost.png')}}" width="30px">
</td>
<td style="text-align: left;">
<a href="{{ url_for('get_whitepaper') }}">
<h2>{{ _("And when you are all set: Get the whitepaper from the timechain.") }}</h2>
</a>
</td>
<td style="text-align: left;">
</td>
</tr>
{% endif %}
</tbody>
</table>
<br>
{% endif %}
Having issues? Try reading <a href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/faq.md" target="_blank" style="color: #fff; font-size: 1.05em; font-weight: bolder;">the FAQ</a><br><br>
Still having issues?<br>
Feel free to <a href="https://github.com/cryptoadvance/specter-desktop/issues/new" target="_blank" style="color: #fff;">open an issue on the GitHub</a> or ask in the <a href="https://t.me/spectersupport" target="_blank" style="color: #fff;">Specter Community telegram chat</a>
{{ _('Having issues? Try reading <a href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/faq.md" target="_blank" style="color: #fff; font-size: 1.05em; font-weight: bolder;">the FAQ</a>') }}<br><br>
{{ _("Still having issues?") }}<br>
{{ _('Feel free to <a href="https://github.com/cryptoadvance/specter-desktop/issues/new" target="_blank" style="color: #fff;">open an issue on the GitHub</a> or ask in the <a href="https://t.me/spectersupport" target="_blank" style="color: #fff;">Specter Community telegram chat</a>') }}
<br><br>
<div class="spacer"></div>
<a href="https://github.com/cryptoadvance/specter-desktop#help-wanted-do-you-like-specter" target="_blank" style="color: #fff; display: inline-block;"><h2 id="help-wanted-text" style="display: inline-block;">Help wanted: Do you like Specter?</h2><img src="static/img/love.png" width="20px" style="vertical-align: sub; margin-left: 10px;"/></a>
<a href="https://github.com/cryptoadvance/specter-desktop#help-wanted-do-you-like-specter" target="_blank" style="color: #fff; display: inline-block;"><h2 id="help-wanted-text" style="display: inline-block;">{{ _('Help wanted: Do you like Specter?') }}</h2><img src="static/img/love.png" width="20px" style="vertical-align: sub; margin-left: 10px;"/></a>
<div class="spacer"></div>
<div class="footer">
Supported and maintained by:<br>
{{ _("Supported and maintained by") }}:<br>
<a href="https://specter.solutions/" target="_blank" style="color: #fff;">
Specter.Solutions
</a>
@ -256,6 +262,9 @@
}
</script>
{% block scripts %}
<script>
{% include "includes/language/language_js.jinja" %}
</script>
{% endblock %}
</body>
</html>

View file

@ -8,9 +8,9 @@
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="text" autocomplete="off" spellcheck="false" id="newtitle" name="newtitle" value="{{ title }}" style="font-size: 1.5em; text-align: center; display: none;" />
<div class="row center">
<button type="submit" class="btn update" id="save" name="action" value="rename" style="display: none; width: 49.2%; margin: 3px;">Update</button>
<button type="button" class="btn cancel" id="cancel" onclick="toggleEdit()" style="display: none; width: 49.2%; margin: 3px;">Cancel</button>
<button type="button" class="btn edit" id="edit" onclick="toggleEdit()">Edit Name</button>
<button type="submit" class="btn update" id="save" name="action" value="rename" style="display: none; width: 49.2%; margin: 3px;">{{ _("Update") }}</button>
<button type="button" class="btn cancel" id="cancel" onclick="toggleEdit()" style="display: none; width: 49.2%; margin: 3px;">{{ _("Cancel") }}</button>
<button type="button" class="btn edit" id="edit" onclick="toggleEdit()">{{ _("Edit Name") }}</button>
</div>
<br>
</form>

View file

@ -35,30 +35,30 @@
<div id="price-popup" class="hidden">
<form id="price-form">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<h1>Price Settings</h1>
<h1>{{ _("Price Settings") }}</h1>
{% if specter.price_check %}
<div class="card">
<p>Please set custom rate or choose between the automated price provider options.</p>
<p>{{ _("Please set custom rate or choose between the automated price provider options.") }}</p>
<nav class="row">
<button type="button" id="manual_price_btn" class="btn radio left checked" onclick="setPriceView('manual')"> Manual </button>
<button type="button" id="automatic_price_btn" class="btn radio right" onclick="setPriceView('automatic')"> Automatic </button>
<button type="button" id="manual_price_btn" class="btn radio left checked" onclick="setPriceView('manual')">{{ _("Manual") }}</button>
<button type="button" id="automatic_price_btn" class="btn radio right" onclick="setPriceView('automatic')">{{ _("Automatic") }}</button>
</nav>
<br>
<div id="manual_price">
Price:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
{{ _("Price:") }}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<input style="max-width: 150px;" type="number" min="0" step="0.01" name="alt_rate" value="{{ specter.alt_rate }}" placeholder="Set the price of bitcoin in your chosen currency">
<br><br>
Symbol:&nbsp;&nbsp;
{{ _("Symbol:") }}&nbsp;&nbsp;
<input style="max-width: 150px;" type="text" name="alt_symbol" value="{{ specter.alt_symbol }}" placeholder="eg. $/€/£">
<br>
</div>
<div id="automatic_price" class="hidden">
<h2>Select price provider:</h2>
<h2>{{ _("Select price provider:") }}</h2>
<table id="auto-price-table" style="border: none; margin: auto;">
<tbody>
<tr style="background: none;">
<td style="padding-bottom: 0px; border: none; text-align: right;">
Currency:
{{ _("Currency:") }}
</td>
<td style="padding-bottom: 0px; border: none; text-align: left;">
<fieldset style="border:none; display: inline; width: 100%;">
@ -91,7 +91,7 @@
</tr>
<tr style="background: none;">
<td style="padding-top: 10px; padding-bottom: 0px; padding-right: 15px; border: none; text-align: right;">
<p>Provider:</p>
<p>{{ _("Provider:") }}</p>
</td>
<td style="padding-top: 10px; padding-bottom: 0px; border: none; text-align: left;">
<fieldset style="border:none; display: inline; width: 100%;">
@ -155,7 +155,7 @@
</tr>
<tr style="background: none;" class="hidden" id="weight_unit_row">
<td style="padding-top: 10px; padding-bottom: 10px; border: none; text-align: right;">
Weight Unit:
{{ _("Weight Unit:") }}
</td>
<td style="padding-top: 10px; padding-bottom: 10px; border: none; text-align: left;">
<fieldset style="border:none; display: inline; width: 100%;">
@ -169,26 +169,26 @@
</tr>
</tbody>
</table>
<p class="note" style="max-width: 300px; margin: auto;">Price will automatically be fetched from the selected provider every 10 minutes.</p>
<p class="note" style="max-width: 300px; margin: auto;">{{ _("Price will automatically be fetched from the selected provider every 10 minutes.") }}</p>
</div>
<br><br>
<input type="hidden" id="price_type" name="price_type" value="{{ 'automatic' if specter.price_provider else 'manual' }}">
<button type="button" class="btn" onclick="savePriceData(true)" style="margin: auto;">Save</button>
<button type="button" class="btn" onclick="savePriceData(true)" style="margin: auto;">{{ _("Save") }}</button>
</div>
<br><br>
<button type="button" class="btn" onclick="togglePriceCheck(false)" style="margin: auto;" >Disable showing price</button>
<button type="button" class="btn" onclick="togglePriceCheck(false)" style="margin: auto;" >{{ _("Disable showing price") }}</button>
{% else %}
<p>You can enable showing price feature to see the price<br>information of your Bitcoin amounts across Specter.</p>
<p>{{ _("You can enable showing price feature to see the price<br>information of your Bitcoin amounts across Specter.") }}</p>
<br>
<button type="button" class="btn" style="margin: auto;" onclick="togglePriceCheck(true)">Enable showing price</button>
<button type="button" class="btn" style="margin: auto;" onclick="togglePriceCheck(true)">{{ _("Enable showing price") }}</button>
{% endif %}
</form>
<script>
async function togglePriceCheck(showPrice) {
if (showPrice) {
showNotification('Turning on show price mode...', 0)
showNotification('{{ _("Turning on show price mode...") }}', 0)
} else {
showNotification('Turning off show price mode...', 0)
showNotification('{{ _("Turning off show price mode...") }}', 0)
}
let url = `{{ url_for('price_endpoint.toggle') }}`;
var formData = new FormData(document.getElementById('price-form'));
@ -209,7 +209,7 @@
location.reload();
return
};
showError('Failed to toggle price mode...')
showError('{{ _("Failed to toggle price mode...") }}')
} catch(e) {
console.log("Caught error: ", e);
showError(e);
@ -217,7 +217,7 @@
}
async function savePriceData(reload) {
showNotification('Updating price data...', 0)
showNotification('{{ _("Updating price data...") }}', 0)
let url = `{{ url_for('price_endpoint.update') }}`;
var formData = new FormData(document.getElementById('price-form'));
try {
@ -237,7 +237,7 @@
location.reload();
return
};
showError('Failed to fetch price data from selected provider, please try again or select another provider.')
showError('{{ _("Failed to fetch price data from selected provider, please try again or select another provider.") }}')
} catch(e) {
console.log("Caught error: ", e);
showError(e);

View file

@ -1,9 +1,9 @@
<div style="display: inline;">
<span>Device Type:</span>
<span>{{ _("Device Type:") }}</span>
<br>
<fieldset style="border:none; padding: 0px; margin: 0px;">
<select name="device_type" id="device_type">
<option value="" selected>Select type</option>
<option value="" selected>{{ _("Select type") }}</option>
{% for cls in specter.device_manager.supported_devices %}
<option value="{{cls.device_type}}">{{cls.name}}</option>
{% endfor %}

View file

@ -18,14 +18,14 @@
<div class="row" style="text-align: center; line-height: 2;">
{% include "device/components/device_type.jinja" %}
</div>
<button type="submit" class="btn hidden" id="settype" name="action" value="settype" style="margin: auto; margin-top: 10px; align-self:center;">Update</button>
<button type="submit" class="btn hidden" id="settype" name="action" value="settype" style="margin: auto; margin-top: 10px; align-self:center;">{{ _("Update") }}</button>
</form>
<br>
<div class="table-holder">
<table id="keys-table">
<thead>
<tr>
<th>Network</th><th>Purpose</th><th class="optional">Derivation</th><th class="mobile-only">Export</th><th class="optional"></th><th class="optional">Key</th><th class="optional">Actions</th>
<th>{{ _("Network") }}</th><th>{{ _("Purpose") }}</th><th class="optional">{{ _("Derivation") }}</th><th class="mobile-only">{{ _("Export") }}</th><th class="optional"></th><th class="optional">{{ _("Key") }}</th><th class="optional">{{ _("Actions") }}</th>
</tr>
</thead>
<input type="hidden" id="key_selected" value="0" />
@ -40,7 +40,7 @@
{% if specter.hide_sensitive_info %}
<td></td>
{% else %}
<td width="15" onclick="showPageOverlay('key_export_qr');document.getElementById('key_selected').value='{{loop.index0}}';toggleKeyDisplay();" title="Show key QR code" style="cursor: pointer;">
<td width="15" onclick="showPageOverlay('key_export_qr');document.getElementById('key_selected').value='{{loop.index0}}';toggleKeyDisplay();" title="{{ _('Show key QR code') }}" style="cursor: pointer;">
<img style="margin-top: 5px; width: 40px;" src="{{ url_for('static', filename='img/qr-code.svg') }}" class="svg-white"/>
</td>
{% endif %}
@ -55,7 +55,7 @@
<form action="./" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="key" value="{{ key.original }}">
<button type="submit" name="action" value="delete_key" class="btn danger hovering">Delete</button>
<button type="submit" name="action" value="delete_key" class="btn danger hovering">{{ _("Delete") }}</button>
</form>
</td>
</tr>
@ -66,38 +66,38 @@
<div style="line-height: 1; margin-top: 30px;">
<form action="./" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<button id="add_keys" type="submit" name="action" value="add_keys" class="btn centered">Add more keys</button>
<button id="add_keys" type="submit" name="action" value="add_keys" class="btn centered">{{ _("Add more keys") }}</button>
</form>
<br>
{% if device.hwi_support %}
<button type="button" class="btn centered" onclick="showPageOverlay('message-signing')">Sign message on device</button>
<button type="button" class="btn centered" onclick="showPageOverlay('message-signing')">{{ _("Sign message on device") }}</button>
<div id="message-signing" class="hidden" style="text-align: left;">
Derivation path:<input type="text" id="messageDerivationPath" type="text" value="" placeholder="e.g. m/84h/0h/0h/0/0"><br><br>
Message:<br>
{{ _("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" style="font-size: 0.95em;"></textarea>
<span id="signature-label" class="hidden">Message signature:</span>
<p style="word-break: break-all; margin-top: 5px;" id="signature" title="Copy message signature" class="explorer-link" onclick="copyText(this.innerText, 'Copied message signature')">
<span id="signature-label" class="hidden">{{ _("Message signature") }}:</span>
<p style="word-break: break-all; margin-top: 5px;" id="signature" title='{{ _("Copy message signature") }}' class="explorer-link" onclick="copyText(this.innerText, '{{ _("Copied message signature") }}')">
</p>
<button type="button" class="btn centered" onclick="signMessageOnDevice();">Sign message on device</button>
<button type="button" class="btn centered" onclick="signMessageOnDevice();">{{ _("Sign message on device") }}</button>
</div>
{% endif %}
{% if specter.is_liquid %}
<br>
<a class="btn centered" href="{{ url_for('devices_endpoint.device_blinding_key', device_alias=device.alias) }}">Update master blinding key</a>
<a class="btn centered" href="{{ url_for('devices_endpoint.device_blinding_key', device_alias=device.alias) }}">{{ _("Update master blinding key") }}</a>
{% endif %}
<br>
{% if device.supports_hwi_toggle_passphrase %}
<button type="button" class="btn centered" onclick="togglePassphrase('{{ device.device_type }}')">Toggle device passphrase</button>
<button type="button" class="btn centered" onclick="togglePassphrase('{{ device.device_type }}')">{{ _("Toggle device passphrase") }}</button>
<br>
{% endif %}
<form action="./" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" name="action" value="forget" class="btn danger centered" id="forget_device">Forget the device</button>
<button type="submit" name="action" value="forget" class="btn danger centered" id="forget_device">{{ _("Forget the device") }}</button>
</form>
</div>
<br>
{% if wallets %}
<h1>Wallets Using This Device</h1>
<h1>{{ _("Wallets Using This Device") }}</h1>
<div class="row overflow">
{% for wallet in wallets %}
<a href="{{ url_for('wallets_endpoint.wallet', wallet_alias=wallet.alias) }}" style="text-decoration: none;">
@ -110,12 +110,12 @@
</div>
{% endif %}
<div class="hidden" id="new_device_popup">
<h1>New device was added successfully!</h1>
<h1>{{ _("New device was added successfully!") }}</h1>
<div class="row overflow">
<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
{{ _("Add another device") }}
</div>
</a>
<form action="{{ url_for('wallets_endpoint.new_wallet', wallet_type='simple') }}" method="POST">
@ -125,7 +125,7 @@
<button type="submit" style="background: none; color: inherit; border: none; padding: 0; font: inherit; cursor: pointer; outline: inherit;">
<div class="small-card radio">
<img src="{{ url_for('static', filename='img/key.svg') }}" class="svg-white" style="width: 120px;">
Create single key wallet
{{ _("Create single key wallet") }}
</div>
</button>
</form>
@ -133,7 +133,7 @@
<a href="{{ url_for('wallets_endpoint.new_wallet', wallet_type='multisig') }}" style="text-decoration: none;">
<div class="small-card radio">
<img src="{{ url_for('static', filename='img/two-keys.svg') }}" class="svg-white" style="width: 120px;">
Create multisignature wallet
{{ _("Create multisignature wallet") }}
</div>
</a>
{% endif %}
@ -173,7 +173,7 @@
document.getElementById('signature-label').style.display = 'block';
document.getElementById('signature').innerText = signature;
showPageOverlay('message-signing');
showNotification('Message was signed successfully!')
showNotification('{{ _("Message was signed successfully!") }}')
}
</script>
{% endblock %}

View file

@ -6,29 +6,29 @@
<div style="width: auto; margin: 40px;" class="card center" >
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<div>
<h1> Get the master blinding key </h1>
<h1> {{ _("Get the master blinding key") }} </h1>
<input type="hidden" name="new_device" value="{{ new_device }}"/>
<input type="hidden" name="blinding_key" id="blinding_key"/>
<div style="width:100%;">
<div id="connect-hwi" style="max-width: 250px; width: 250px; margin: 20px auto;" class="btn {% if not device.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
{{ _("Get via USB") }}
</div>
<qr-scanner id="key-scan" {% if not device.qr_code_support %}class="hidden"{% endif %}>
<a slot="button" href="#" class="btn" style="max-width: 250px; width: 250px; margin: 20px auto;">
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 26px; margin: 0px;" class="svg-white">
Scan QR code
{{ _("Scan QR code") }}
</a>
</qr-scanner>
<button {% if device.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: 20px auto;">
<img src="{{ url_for('static', filename='img/copy.svg') }}" style="width: 22px; margin: 0px;" class="svg-white">
Paste xpub
{{ _("Paste xpub") }}
</button>
<div class="hidden" id="paste-xpub-popup">
<h1>Paste xpub</h1>
<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='';" class="btn centered">Add xpub</button>
<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>
</div>

View file

@ -46,49 +46,49 @@
<input type="hidden" name="range_end" value="{{ range_end }}"/>
{% endif %}
<div>
<h1 style="font-size: 1.8em;">Upload Keys</h1>
<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>
<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" {%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>
{{ _("Connect your ColdCard to the computer via USB and unlock it or upload a wallet export file from micro SD card.") }}<br>
</p>
<span class="note">
To export the wallet data file from the ColdCard, insert your micro SD card into the device, then:<br>
{{ _("To export the wallet data file from the ColdCard, insert your micro SD card into the device, then:<br>
To get the single sig xpubs, go to: Advanced -> MicroSD Card -> Export Wallet -> Generic JSON. When asked for account, select account number (default is 0) and confirm the export.<br>
To get the multisig xpubs, go to: Settings -> Multisig Wallets -> Export XPUB and confirm the export.
To get the multisig xpubs, go to: Settings -> Multisig Wallets -> Export XPUB and confirm the export.") }}
</span>
</div>
<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>
{{ _("Connect your Specter-DIY to the computer via USB and unlock it or scan the wallet master public key.") }}<br>
</p>
<span class="note">
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.
{{ _("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" {%if device_class.device_type != 'cobo' %}class="hidden"{% endif %}>
<p>
Scan your Cobo Vault master public key or upload a wallet export file.<br>
{{ _("Scan your Cobo Vault master public key or upload a wallet export file.") }}<br>
</p>
<span class="note">
To get the master public keys QR codes to scan:<br><br>
{{ _("To get the master public keys QR codes to scan:<br><br>
For single sig, click on: Setting -> Watch-Only Wallet -> Generic Wallet, Click the top right 3 dots (...) -> Wallet Info ->Toggle Address Type, then select the wallet address type you would like to use for your wallet.<br><br>
For multisig, click on: Multisig Wallet -> Click the top right 3 dots (...) -> Show/Export XPUB, then select the wallet address type you would like to use for your wallet.<br>
Cobo Vault will then display the QR code which you should scan into Specter.<br><br>
To import with SD card, click on "touch here to export the file with microSD" on the same screen as the QR code.
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" {%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>
<p>{{ _("Connect your hardware device to the computer via USB.") }}</p>
</div>
<br>
<table>
<thead>
<tr>
<th></th><th>Purpose</th><th>Derivation</th><th>XPUB</th><th><button type="button" class="btn" style="width: 60px;" id="edit-xpubs-table-btn" onclick="editXpubsTable(this)">Edit</button></th>
<th></th><th>{{ _("Purpose") }}</th><th>{{ _("Derivation") }}</th><th>XPUB</th><th><button type="button" class="btn" style="width: 60px;" id="edit-xpubs-table-btn" onclick="editXpubsTable(this)">{{ _("Edit") }}</button></th>
</tr>
</thead>
<tbody id="xpubs_table">
@ -99,35 +99,35 @@
<div class="row overflow">
<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
{{ _("Get via USB") }}
</div>
<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
{{ _("Upload from SD Card") }}
</div>
</label>
<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
{{ _("Scan QR code") }}
</a>
</qr-scanner>
<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
{{ _("Paste xpub") }}
</button>
<div class="hidden" id="paste-xpub-popup">
<h1>Paste xpub</h1>
<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='';" class="btn centered">Add xpub</button>
<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>
</div>
</div>
<button type="submit" id="submit-keys" class="btn centered action" style="width: 140px; max-width: 140px; margin: auto;">Continue</button>
<button type="submit" id="submit-keys" class="btn centered action" style="width: 140px; max-width: 140px; margin: auto;">{{ _("Continue") }}</button>
<br><br>
</form>
{% endblock %}
@ -155,7 +155,7 @@
xpubsTableRowsInput.value = xpubsTableRows;
let xpubRowHTML = `
<td></td>
<td><input id="xpubs-table-row-${xpubsTableRows}-purpose" name="xpubs-table-row-${xpubsTableRows}-purpose" type="text" value="${purpose}" placeholder="XPUB purpose" style="width: 90%; min-width: 100px;"></td>
<td><input id="xpubs-table-row-${xpubsTableRows}-purpose" name="xpubs-table-row-${xpubsTableRows}-purpose" type="text" value="${purpose}" placeholder="{{ _("XPUB purpose") }}" style="width: 90%; min-width: 100px;"></td>
<td id="xpubs-table-row-${xpubsTableRows}-derivation" style="min-width: 0px;">${derivation}</td>
<input id="xpubs-table-row-${xpubsTableRows}-derivation-hidden" name="xpubs-table-row-${xpubsTableRows}-derivation-hidden" type="hidden" value="${derivation}">
<td id="xpubs-table-row-${xpubsTableRows}-xpub" class="xpub scroll" style="overflow: auto;">${xpub}</td>
@ -177,9 +177,9 @@
} else {
let xpubPurpose = 'Custom'
if (xpub.startsWith("ypub") || xpub.startsWith("upub")) {
xpubPurpose = '#0 Single Sig (Nested)';
xpubPurpose = '{{ _("#0 Single Sig (Nested)") }}';
} else if (xpub.startsWith("vpub") || xpub.startsWith("zpub")) {
xpubPurpose = '#0 Single Sig (Segwit)';
xpubPurpose = '{{ _("#0 Single Sig (Segwit)") }}';
}
addXpub(xpubPurpose, 'm/', xpub)
}
@ -198,8 +198,8 @@
let xpubAddAccountRowHTML = `
<td></td>
<td></td>
<td><input id="account_number_xpubs" value="0" min="0" type="number" step="1" style="width: 150px;" placeholder="Account #"></td>
<td><button type="button" class="btn" style="width: 150px;" onclick="addAccountXpubs()">Add account</button></td>
<td><input id="account_number_xpubs" value="0" min="0" type="number" step="1" style="width: 150px;" placeholder="{{ _("Account #") }}"></td>
<td><button type="button" class="btn" style="width: 150px;" onclick="addAccountXpubs()">{{ _("Add account") }}</button></td>
<td></td>
`
let xpubAddAccountRow = document.createElement('tr');
@ -210,10 +210,10 @@
let xpubAddCustomRowHTML = `
<td></td>
<td><input id="new_xpub_purpose" type="text" placeholder="XPUB purpose"></td>
<td><input id="new_xpub_purpose" type="text" placeholder="{{ _("XPUB purpose") }}"></td>
<td><input id="new_xpub_derivation" type="text" placeholder="m/..."></td>
<td></td>
<td><button type="button" class="btn" style="width: 60px;" onclick="addCustomDerivation()">Add</button></td>
<td><button type="button" class="btn" style="width: 60px;" onclick="addCustomDerivation()">{{ _("Add") }}</button></td>
`
let xpubAddCustomRow = document.createElement('tr');
xpubAddCustomRow.id = `edit_custom_der`;
@ -224,8 +224,8 @@
let xpubAddRowHTML = `
<td></td>
<td></td>
<td><button type="button" class="btn" style="width: 150px;" onclick="document.getElementById('edit_add_account').style.display='table-row';">Add account</button></td>
<td><button type="button" class="btn" style="width: 150px;" onclick="document.getElementById('edit_custom_der').style.display='table-row';">Add custom derivation</button></td>
<td><button type="button" class="btn" style="width: 150px;" onclick="document.getElementById('edit_add_account').style.display='table-row';">{{ _("Add account") }}</button></td>
<td><button type="button" class="btn" style="width: 150px;" onclick="document.getElementById('edit_custom_der').style.display='table-row';">{{ _("Add custom derivation") }}</button></td>
<td></td>
`
let xpubAddRow = document.createElement('tr');
@ -275,11 +275,11 @@
function editXpubsTable(editBtn) {
let visibility = '';
if (editBtn.innerHTML == 'Edit') {
editBtn.innerHTML = 'Done';
if (editBtn.innerHTML == '{{ _("Edit") }}') {
editBtn.innerHTML = '{{ _("Done") }}';
visibility = 'visible';
} else {
editBtn.innerHTML = 'Edit';
editBtn.innerHTML = '{{ _("Edit") }}';
visibility = 'hidden';
}
document.getElementById('edit_select_add_method').style.display = 'table-row';
@ -308,7 +308,7 @@
}
// this shouldn't happen actually
if(devices.length == 0){
showError("No devices found :(");
showError('{{ _("No devices found :(") }}');
return;
}
@ -342,7 +342,7 @@
"{specter.chain}"
);
if(xpub == null){
showError('Failed to retrive device data. Please try again.');
showError('{{ _("Failed to retrive device data. Please try again.") }}');
return;
}
@ -520,7 +520,7 @@
let str = `[${obj.xfp}/${path}]${obj.xpub}`;
addXpubs(str);
}else{
showError("Unknown key format");
showError('{{ _("Unknown key format") }}');
}
}else{
addXpubs(result);

View file

@ -34,7 +34,7 @@
</style>
<form id="form" action="{{ url_for('devices_endpoint.new_device_mnemonic', device_type=device_type) }}" 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>
<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 %}
@ -44,20 +44,20 @@
<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.
{{ _('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 %}
<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>
<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>
<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;">
@ -66,7 +66,7 @@
<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>
<button type="button" class="btn" style="max-width: 200px; margin-top: 9px;" onclick="generateMnemonic()">{{ _("Generate New Mnemonic") }}</button>
</div>
<br>
<table id="mnemonic_table">
@ -82,25 +82,25 @@
</div>
</div>
<br>
<span id="toggle-advanced" style="cursor: pointer;" onclick="toggleAdvanced()">Advanced &#9654;</span>
<span id="toggle-advanced" style="cursor: pointer;" onclick="toggleAdvanced()">{{ _("Advanced") }} &#9654;</span>
<br>
<br>
<div id="advanced-settings" class="hidden" style="width: 400px; margin: auto;">
<label>Encrypt Wallet File: </label>
<label>{{ _("Encrypt Wallet File:") }} </label>
<input type="password" name="file_password" class="inline" placeholder="password">
<br>
<br>
<label>BIP39 passphrase (optional): </label>
<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>
<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>
<button type="submit" id="submit-mnemonic" class="btn centered action" style="width: 140px; max-width: 140px; margin: auto;">{{ _("Continue") }}</button>
<br><br>
</form>
{% endblock %}
@ -181,10 +181,10 @@
let advancedSettings = document.getElementById('advanced-settings');
if (advancedSettings.style.display === 'block') {
advancedSettings.style.display = 'none';
advancedButton.innerHTML = 'Advanced &#9654;';
advancedButton.innerHTML = '{{ _("Advanced") }} &#9654;';
} else {
advancedSettings.style.display = 'block';
advancedButton.innerHTML = 'Advanced &#9660;';
advancedButton.innerHTML = '{{ _("Advanced") }} &#9660;';
}
}
</script>

View file

@ -1,10 +1,10 @@
{% extends "base.jinja" %}
{% block main %}
<h1 style="font-size: 2em;">Add Device</h1><br>
<h1 style="font-size: 2em;">{{ _("Add Device") }}</h1><br>
<div id="device_setup_wizard" class="card center" style="width: auto; min-width: 90%; margin: 40px;">
<div id="device-type-container">
<h1 style="font-size: 1.8em;">Select Your Device Type</h1>
<input id="device-type-searchbar" type="text" placeholder="Filter devices..." style="width: 60%; font-size: 1.4em; padding: 10px;" oninput="filterDeviceTypes(this.value)" {% if (specter.device_manager.supported_devices_for_chain(specter)|length) < 8 %}class="hidden"{% endif %}>
<h1 style="font-size: 1.8em;">{{ _("Select Your Device Type") }}</h1>
<input id="device-type-searchbar" type="text" placeholder='{{ _("Filter devices...") }}' style="width: 60%; font-size: 1.4em; padding: 10px;" oninput="filterDeviceTypes(this.value)" {% if (specter.device_manager.supported_devices_for_chain(specter)|length) < 8 %}class="hidden"{% endif %}>
<div id="devices_list" class="row overflow">
{% for cls in specter.device_manager.supported_devices_for_chain(specter) %}
{% if not cls.hot_wallet or specter.rpc %}
@ -17,7 +17,7 @@
{% endif %}
{% endfor %}
</div>
<a class="note center" style="text-decoration: underline; cursor: pointer;" href="{{ url_for('devices_endpoint.new_device_manual') }}">Manual configuration</a>
<a class="note center" style="text-decoration: underline; cursor: pointer;" href="{{ url_for('devices_endpoint.new_device_manual') }}">{{ _("Manual configuration") }}</a>
</div>
</div>
{% endblock %}

View file

@ -4,50 +4,50 @@
{% include "includes/qr-scanner.html" %}
{% if device %}
<h1>Adding keys to {{ device.name }}</h1>
<h1>{{ _("Adding keys to") }} {{ device.name }}</h1>
{% else %}
<h1>Setting up a new device</h1>
<h1>{{ _("Setting up a new device") }}</h1>
{% endif %}
<div class="card">
<form action="./" method="POST" onsubmit="{% if not device %}return checkType();{% endif %}">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
{% if not device %}
<label>Name it:</label>
<label>{{ _("Name it") }}:</label>
<div class="row">
<input type="text" id="device_name" name="device_name" value="{{ device_name }}" placeholder="Name your device">
</div>
<br>
{% include "device/components/device_type.jinja" %}
<p class="warning hidden" id="type_reminder"><img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>Please make sure to choose the device type before continuing</p>
<p class="warning hidden" id="type_reminder"><img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>{{ _("Please make sure to choose the device type before continuing") }}</p>
<div id="toggle_passphrase_container" class="hidden">
<p>Device Management:</p>
<p>{{ _("Device Management") }}:</p>
<div class="row">
<button type="button" class="btn centered" onclick="togglePassphrase(document.getElementById('device_type').value)">Toggle device passphrase</button>
<button type="button" class="btn centered" onclick="togglePassphrase(document.getElementById('device_type').value)">{{ _("Toggle device passphrase") }}</button>
</div>
</div>
{% endif %}
{% if not device or not device.hot_wallet %}
<div id="cold_device">
<br>
<h2>Scan, paste or load xpubs:</h2>
<h2>{{ _("Scan, paste or load xpubs") }}:</h2>
<div class="note">
{% if not device %}
You will be able add more keys later, from the device menu.
{{ _("You will be able add more keys later from the device menu.") }}
{% endif %}
</div>
<p>One line per xpub. Ideally with derivation path in the form: <span style="font-style: italic; font-weight: bold; font-size: 15px;"><br>[ fingerprint / derivation / path ] xpub.</span></p>
<textarea style="margin: 5px;" id="txt" name="xpubs" placeholder="Enter your xpubs here">{{ xpubs }}</textarea>
<p>{{ _("One line per xpub. Ideally with derivation path in the form") }}: <span style="font-style: italic; font-weight: bold; font-size: 15px;"><br>[ fingerprint / derivation / path ] xpub.</span></p>
<textarea style="margin: 5px;" id="txt" name="xpubs" placeholder='{{ _("Enter your xpubs here") }}'>{{ xpubs }}</textarea>
<div class="row">
<qr-scanner id="xpub-scan" style="width:50%">
<a slot="button" href="#" class="btn" style="margin: 8px;">
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 26px; margin-right: 2px;" class="svg-white"> Scan
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 26px; margin-right: 2px;" class="svg-white"> {{ _("Scan") }}
<div class="tool-tip">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Using an airgapped device with QR codes<br><br></span>
Scan QR codes with extended public keys one by one.<br><br>
We recommend importing <b>Native Segwit</b> and <b>Segwit Multisig</b> keys.
<span class="info__title">{{ _("Using an airgapped device with QR codes") }}<br><br></span>
{{ _("Scan QR codes with extended public keys one by one.") }}<br><br>
{{ _("We recommend importing <b>Native Segwit</b> and <b>Segwit Multisig</b> keys.") }}
</span>
</p>
</div>
@ -56,17 +56,17 @@
<input type="file" id="file" class="inputfile" multiple/>
<label for="file" class="btn" style="margin: 8px;">
<img src="{{ url_for('static', filename='img/file.svg') }}" style="width: 26px; margin-right: 2px;" class="svg-white">
Choose files
{{ _("Choose files") }}
<div class="tool-tip">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Using an airgapped ColdCard with MicroSD<br><br></span>
Export extended public keys for single-key wallets to the SD card from<br>
<span class="info__title">{{ _("Using an airgapped ColdCard with MicroSD") }}<br><br></span>
{{ _("Export extended public keys for single-key wallets to the SD card from") }}<br>
<b>Advanced → MicroSD → Export Wallet → Generic JSON</b><br><br>
To export multisignature pubkeys go to <br>
{{ _("To export multisignature pubkeys go to") }} <br>
<b>Settings → Multisig Wallets → Export XPUB</b>
We recommend using Native Segwit.
{{ _("We recommend using Native Segwit.") }}
</span>
</p>
</div>
@ -74,45 +74,45 @@
</div>
<div class="row">
<button type="button" class="btn" style="margin: 8px;" id="askhwi">
<img src="{{ url_for('static', filename='img/usb.svg') }}" style="width: 26px; margin-right: 2px;" class="svg-white"> Get over USB
<img src="{{ url_for('static', filename='img/usb.svg') }}" style="width: 26px; margin-right: 2px;" class="svg-white"> {{ _("Get via USB") }}
</button>
</div>
<p class="note center" style="text-decoration: underline; cursor: pointer;" onclick="showPageOverlay('usb_import_account_number')">Specify account number (advanced)</p>
<p class="note center" style="text-decoration: underline; cursor: pointer;" onclick="showPageOverlay('usb_import_account_number')">{{ _("Specify account number (advanced)") }}</p>
<div class="hidden" id="usb_import_account_number">
<h1>Select the account you wish to import</h1>
<p class="center" style="max-width: 500px; margin: 5px auto;">You can set here which account number you wish to import.<br>Account derivation uses the number choosen here to derive an xpub as specified in <a style="color: #fff" href="https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account" target="_blank">BIP44</a>.</p>
<h1>{{ _("Select the account you wish to import") }}</h1>
<p class="center" style="max-width: 500px; margin: 5px auto;">{{ _("You can set which account number you wish to import.<br>Account derivation uses the number choosen here to derive an xpub as specified in") }} <a style="color: #fff" href="https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account" target="_blank">BIP44</a>.</p>
Account # <input type="number" step="1" min="0" value="0" id="usb_account_number" style="max-width: 100px; min-width: 100px;"><br><br>
<p class="note center">This is an advanced feature.<br>If you're not familiar with account derivations please leave this as 0.</p><br>
<button class="btn centered" type="button" onclick="hidePageOverlay()">Close</button>
<p class="note center">{{ _("This is an advanced feature.<br>If you're not familiar with account derivations please leave this as 0.") }}</p><br>
<button class="btn centered" type="button" onclick="hidePageOverlay()">{{ _("Close") }}</button>
</div>
<div class="note">
Examples:<br>
{{ _("Examples") }}:<br>
<pre style="font-size: 1em">
[f79ec910/84'/1'/0']tpubDA5h1hw24fzoBi9...pR1DiM4EBu
[F79EC910/48h/1h/0h/2h]Vpub5DRk6RvRAjZf9rmZV1Q8...TZSDQL67e5i
upub5En4f7k8gaG2KDHvBeEYox...rFpJRHpiZ4DE
</pre>
</div><br>
<button type="submit" class="btn centered" name="action" value="newcolddevice" onsubmit="showPacman()">Continue</button>
<button type="submit" class="btn centered" name="action" value="newcolddevice" onsubmit="showPacman()">{{ _("Continue") }}</button>
</div>
{% endif %}
{% if not device or device.hot_wallet %}
<div id="hot_device" class="hidden">
<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.
<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>
<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>
<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>
<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;">
@ -121,7 +121,7 @@ upub5En4f7k8gaG2KDHvBeEYox...rFpJRHpiZ4DE
<option value="256">24 words</option>
</select>
</fieldset>
<button type="submit" class="btn" name="action" value="generatemnemonic" style="margin: auto;max-width: 200px;">Generate New Mnemonic</button>
<button type="submit" class="btn" name="action" value="generatemnemonic" style="margin: auto;max-width: 200px;">{{ _("Generate New Mnemonic") }}</button>
</div>
<br>
<table>
@ -136,36 +136,34 @@ upub5En4f7k8gaG2KDHvBeEYox...rFpJRHpiZ4DE
</table>
</div>
<br>
<label>Encrypt Wallet File: </label>
<label>{{ _("Encrypt Wallet File") }}: </label>
<input type="password" name="file_password" class="inline" placeholder="password">
<br><br>
<span id="toggle_advanced" style="cursor: pointer;">Advanced &#9654;</span>
<span id="toggle_advanced" style="cursor: pointer;">{{ _("Advanced") }} &#9654;</span>
<br><br>
<div id="advanced_settings" class="hidden">
<label>BIP39 passphrase (optional): </label>
<label>{{ _("BIP39 passphrase (optional)") }}: </label>
<input type="password" name="passphrase" class="inline" placeholder="passphrase">
<br>
<p>Derivation paths:<br>
<p>{{ _("Derivation paths") }}:<br>
<span class="note">
Enter each derivation path as a separate line.
Start each path with an <code>m</code>.
No trailing slash.</span>
{{ _("Enter each derivation path as a separate line. Start each path with an <code>m</code>. No trailing slash.") }}</span>
</p>
<textarea name="derivation_paths" placeholder="Specify derivation paths to import for the wallet">
<textarea name="derivation_paths" placeholder='{{ _("Specify derivation paths to import for the wallet") }}'>
m/84h/{{ 0 if specter.info.chain == "main" else 1 }}h/0h
m/49h/{{ 0 if specter.info.chain == "main" else 1 }}h/0h
m/48h/{{ 0 if specter.info.chain == "main" else 1 }}h/0h/1h
m/48h/{{ 0 if specter.info.chain == "main" else 1 }}h/0h/2h</textarea>
<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>
<p style="margin-bottom: 10px;">{{ _("Address range to import") }}: </p>
{{ _("From") }}: <input type="number" name="range_start" class="inline" placeholder="start" value=0 step=1 min=0 max=100000>
<br class="mobile-only">
<br class="mobile-only">
to: <input type="number" name="range_end" class="inline" placeholder="start" value=1000 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>
<br><br>
<button type="submit" class="btn centered" name="action" value="newhotdevice">Continue</button>
<button type="submit" class="btn centered" name="action" value="newhotdevice">{{ _("Continue") }}</button>
</div>
{% endif %}
</form>
@ -179,13 +177,13 @@ m/48h/{{ 0 if specter.info.chain == "main" else 1 }}h/0h/2h</textarea>
let advancedSettings = document.getElementById('advanced_settings');
if (advancedSettings.style.display === 'block') {
advancedSettings.style.display = 'none';
advancedButton.innerHTML = 'Advanced &#9654;';
advancedButton.innerHTML = '{{ _("Advanced") }} &#9654;';
if (isCoinSelectionActive()) {
toggleExpand();
}
} else {
advancedSettings.style.display = 'block';
advancedButton.innerHTML = 'Advanced &#9660;';
advancedButton.innerHTML = '{{ _("Advanced") }} &#9660;';
}
}
@ -287,7 +285,7 @@ m/48h/{{ 0 if specter.info.chain == "main" else 1 }}h/0h/2h</textarea>
}
// this shouldn't happen actually
if(devices.length == 0){
showError("No devices found :(");
showError('{{ _("No devices found") }} :(');
return;
}
console.log(devices)
@ -433,7 +431,7 @@ m/48h/{{ 0 if specter.info.chain == "main" else 1 }}h/0h/2h</textarea>
addKeys(str);
setDeviceType('cobo');
}else{
showError("Unknown key format");
showError('{{ _("Unknown key format") }}');
}
}else{
addKeys(result);

View file

@ -8,26 +8,26 @@
<div class="card" style="width: 600px;">
<form action="." method="POST" role="form">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<h1>HWI Bridge Settings</h1>
<h1>{{ _("HWI Bridge Settings") }}</h1>
{% if tor_service_id %}
<img style="width: 28px; padding:3px;float: left;margin-right: 5px;" src="{{ url_for('static', filename='img/tor.svg') }}"/>
<span style="float: left;">
HWIBridge is also accessible over Tor:<br>
<span title="Copy Tor address" class="explorer-link" onclick="copyText('{{ tor_service_id }}.onion', 'Copied Tor hidden service address: {{ tor_service_id }}.onion')">
{{ _("HWI Bridge is also accessible over Tor") }}:<br>
<span title="Copy Tor address" class="explorer-link" onclick="copyText('{{ tor_service_id }}.onion', '{{ _("Copied Tor hidden service address") }}: {{ tor_service_id }}.onion')">
{{ tor_service_id }}.onion
</span>
</span><br><br><br>
{% endif %}
<label>Whitelisted domains</label>
<label>{{ _("Whitelisted domains") }}</label>
<textarea class="form-control" placeholder="e.i. http://127.0.0.1:25441" name="whitelisted_domains">{{ whitelisted_domains }}</textarea>
<div class="note">
To whitelist multiple domains, just list them one after another, each in a new line, for example:
{{ _("To whitelist multiple domains, just list them one after another, each in a new line, for example") }}:
<br>http://127.0.0.1:25440<br>http://127.0.0.1:25441<br>http://127.0.0.1:25442
</div>
<br>
<br>
<div class="row">
<button type="submit" class="btn" name="action" value="update">Update</button>
<button type="submit" class="btn" name="action" value="update">{{ _("Update") }}</button>
</div>
</form>
</div>

View file

@ -15,7 +15,7 @@
}
</style>
<div class="address-data">
<h1>Address details</h1><br>
<h1>{{ _("Address details") }}</h1><br>
<div class="address-data-info"></div>
<div class="note"></div>
</div>
@ -41,7 +41,7 @@
this.amountPrice = this.getAttribute('data-amount-price');
this.address = this.getAttribute('data-address');
this.wallet = this.getAttribute('data-address-wallet');
this.note.innerText = `Loading address: ${this.address} details...`;
this.note.innerText = `{{ _("Loading address") }}: ${this.address} {{ _("details") }}...`;
fetchAddressData(this);
// Attach the created element to the shadow dom
@ -79,14 +79,14 @@
<qr-code style="margin: auto;" value="bitcoin:${address}" width="256"></qr-code><br>
<table class="address-data-table">
<tbody>
<tr><td>Address:</td><td style="word-break: break-all;"><explorer-link data-type="address" data-value="${address}"></explorer-link></td></tr>
<tr><td>Label:</td><td><address-label data-copy-hidden="true" data-address="${address}" data-wallet="${self.wallet}"></address-label><br></td></tr>
<tr><td>From wallet:</td><td><a href=${walletLink}>${walletName}<a></td></tr>
<tr><td>Address index:</td><td>${addressIndex}</td></tr>
<tr><td>Is change address:</td><td>${isChange ? 'Yes' : 'No'}</td></tr>
<tr><td>Used:</td><td>${self.used ? 'Yes' : 'No'}</td></tr>
<tr><td>UTXO count:</td><td>${self.utxo}</td></tr>
<tr><td>Amount:</td><td>${self.amount} <span class="amount-price note ${self.amountPrice ? '' : 'hidden'}">${self.amountPrice}</span></td></tr>
<tr><td>{{ _("Address") }}:</td><td style="word-break: break-all;"><explorer-link data-type="address" data-value="${address}"></explorer-link></td></tr>
<tr><td>{{ _("Label") }}:</td><td><address-label data-copy-hidden="true" data-address="${address}" data-wallet="${self.wallet}"></address-label><br></td></tr>
<tr><td>{{ _("From wallet") }}:</td><td><a href=${walletLink}>${walletName}<a></td></tr>
<tr><td>{{ _("Address index") }}:</td><td>${addressIndex}</td></tr>
<tr><td>{{ _("Is change address") }}:</td><td>${isChange ? '{{ _("Yes") }}' : '{{ _("No") }}'}</td></tr>
<tr><td>{{ _("Used") }}:</td><td>${self.used ? 'Yes' : 'No'}</td></tr>
<tr><td>{{ _("UTXO count") }}:</td><td>${self.utxo}</td></tr>
<tr><td>{{ _("Amount") }}:</td><td>${self.amount} <span class="amount-price note ${self.amountPrice ? '' : 'hidden'}">${self.amountPrice}</span></td></tr>
</tbody>
</table>
`
@ -94,44 +94,44 @@
if (self.isVerifyHwi) {
addressInfoHTML += `
<br>
<button type="button" onclick='hidePageOverlay();displayAddressOnDevice("${address}", unescape("${escape(JSON.stringify(descriptor))}"))' class="btn" style="min-width:200px; max-width:300px; margin: auto;">Verify address on device</button>`;
<button type="button" onclick='hidePageOverlay();displayAddressOnDevice("${address}", unescape("${escape(JSON.stringify(descriptor))}"))' class="btn" style="min-width:200px; max-width:300px; margin: auto;">{{ _("Verify address on device") }}</button>`;
}
if (self.isVerifyQR) {
addressInfoHTML += `
<p>Or scan this QR code:</p>
<p>{{ _("Or scan this QR code") }}:</p>
<qr-code value="bitcoin:${address}?index=${addressIndex}" width="256" scalable></qr-code><br>`;
}
addressInfoHTML += `
<br>
<h2>Address descriptor</h2>
<h2>{{ _("Address descriptor") }}</h2>
<br>
<div>
<span class="switch-text">Show raw public keys&nbsp;</span>
<span class="switch-text">{{ _("Show raw public keys") }}&nbsp;</span>
<label class="switch">
<input type="checkbox" class="descriptor-switch" checked>
<span class="slider"></span>
</label><br><br>
</div>
<pre class="descriptor"><code class="explorer-link" onclick='copyText(unescape("${escape(descriptor.descriptor)}"), "Copied address descriptor")'>${descriptor.descriptor}</code></pre>
<pre class="descriptor"><code class="explorer-link" onclick='copyText(unescape("${escape(descriptor.descriptor)}"), "{{ _("Copied address descriptor") }}")'>${descriptor.descriptor}</code></pre>
`;
self.info.innerHTML = addressInfoHTML;
self.note.innerText = "";
self.info.querySelector('.descriptor-switch').onchange = (e) => {
if (e.target.checked) {
self.info.querySelector(".descriptor").innerHTML = `<code class="explorer-link" onclick='copyText(unescape("${escape(descriptor.descriptor)}"), "Copied address descriptor")'>${descriptor.descriptor}</code>`;
self.info.querySelector(".descriptor").innerHTML = `<code class="explorer-link" onclick='copyText(unescape("${escape(descriptor.descriptor)}"), "{{ _("Copied address descriptor") }}")'>${descriptor.descriptor}</code>`;
} else {
self.info.querySelector(".descriptor").innerHTML = `<code class="explorer-link" onclick='copyText(unescape("${escape(descriptor.xpubs_descriptor)}"), "Copied address descriptor")'>${descriptor.xpubs_descriptor}</code>`;
self.info.querySelector(".descriptor").innerHTML = `<code class="explorer-link" onclick='copyText(unescape("${escape(descriptor.xpubs_descriptor)}"), "{{ _("Copied address descripto") }}")'>${descriptor.xpubs_descriptor}</code>`;
}
}
return;
}
self.note.innerText = "Failed to load address details...";
self.note.innerText = '{{ _("Failed to load address details...") }}';
} catch(e) {
self.note.innerText = "Failed to load address details...";
self.note.innerText = '{{ _("Failed to load address details...") }}';
console.error(e);
}
}

View file

@ -60,10 +60,10 @@
</style>
<form class="address-label-form">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<a target="_blank" class="explorer-link"><span class="label" autocomplete="off" spellcheck="false">Fetching address label...</span></a>
<a target="_blank" class="explorer-link"><span class="label" autocomplete="off" spellcheck="false">{{ _("Fetching address label...") }}</span></a>
<button type="button" class="btn edit" title="Edit label"><img src="{{ url_for('static', filename='img/edit.svg') }}" style="width: 22px; margin-bottom:-5px;" class="svg-white"/></button>
<button type="button" class="btn update hidden">Update</button>
<button type="button" class="btn cancel hidden">Cancel</button>
<button type="button" class="btn update hidden">{{ _("Update") }}</button>
<button type="button" class="btn cancel hidden">{{ _("Cancel") }}</button>
</form>
</template>
@ -154,7 +154,7 @@
this.label.innerText = this.address
}
} else {
showError('Failed to update address label. Please refresh the page and try again.')
showError('{{ _("Failed to update address label. Please refresh the page and try again.") }}')
}
}
@ -195,7 +195,7 @@
self.explorerLink.onclick = () => {return true;}
} else {
self.explorerLink.onclick = () => {
copyText(self.address, `Copied address: ${ self.address }`);
copyText(self.address, `{{ _("Copied address") }}: ${ self.address }`);
}
}
}
@ -231,10 +231,10 @@
}
return;
}
showError('Failed to fetch data. Please refresh the page and retry.');
showError('{{ _("Failed to fetch data. Please refresh the page and retry.") }}');
} catch(e) {
console.log("Caught error fetching address label: ", e);
showError('Failed to fetch data. Please refresh the page and retry.');
console.log('{{ _("Caught error fetching address label") }}: ', e);
showError('{{ _("Failed to fetch data. Please refresh the page and retry.") }}');
}
}

View file

@ -19,7 +19,7 @@
<td class="used optional"></td>
<td class="utxo optional"></td>
<td class="optional"><span class="amount"></span> <span class="amount-price note hidden">()</span></td>
<td class="verify optional"><button type="button" class="btn" style="width:150px; max-width:150px;margin: 0px;">Verify on device</button></td>
<td class="verify optional"><button type="button" class="btn" style="width:150px; max-width:150px;margin: 0px;">{{ _("Verify on device") }}</button></td>
<td></td>
</tr>
</template>
@ -111,9 +111,9 @@
displayAddressOnDevice(this.addressData.address, JSON.stringify(descriptor));
return;
}
showError("Failed to load address data to display...");
showError('{{ _("Failed to load address data to display...") }}');
} catch(e) {
showError("Failed to load address data to display...");
showError('{{ _("Failed to load address data to display...") }}');
showError(e);
}
}

View file

@ -95,16 +95,16 @@
<nav class="row">
<button type="button" class="receive-addresses-view-btn btn radio left checked">
Receive Addresses
{{ _("Receive Addresses") }}
</button>
<button type="button" class="change-addresses-view-btn btn radio right">
Change Addresses
{{ _("Change Addresses") }}
</button>
</nav>
<div class="pagination-container">
<a class="pagination-back pagination-arrow pagination-disabled">&#8249;</a>
Page <input class="pagination-idx" type="number" step="1" min="1" value="1"></input><span
{{ _("Page") }} <input class="pagination-idx" type="number" step="1" min="1" value="1"></input><span
class="pagination-counter"></span>
<a class="pagination-next pagination-arrow pagination-disabled">&#8250;</a>
<br>
@ -113,7 +113,7 @@
{% include "includes/page-limit-select.html" %}
</div>
<div class="export-container hidden">
<h1>Export addresses to CSV</h1>
<h1>{{ _("Export addresses to CSV") }}</h1>
@ -127,7 +127,7 @@
<a class="export btn" download>Export</a>
<a class="export btn" download>{{ _("Export") }}</a>
</div>
<table class="addresses-table">
@ -137,28 +137,28 @@
<th value="index" class="index-header">
<div class="index-arrow down-arrow"></div>
</th>
<th value="address" class="address-header">Address<div class="address-arrow"></div>
<th value="address" class="address-header">{{ _("Address") }}<div class="address-arrow"></div>
</th>
<th value="label" class="label-header optional">Label<div class="label-arrow">
<th value="label" class="label-header optional">{{ _("Label") }}<div class="label-arrow">
</th>
<th value="used" class="used-header optional">Used<div class="used-arrow">
<th value="used" class="used-header optional">{{ _("Used") }}<div class="used-arrow">
</th>
<th value="utxo" class="utxo-header optional">UTXO<div class="utxo-arrow">
</th>
<th value="amount" class="amount-header optional">Amount<div class="amount-arrow">
<th value="amount" class="amount-header optional">{{ _("Amount") }}<div class="amount-arrow">
</th>
{% if supports_hwi != [] and (supports_hwi_multisig_display_address != [] or not wallet.is_multisig) %}
<th class="optional"></th>
{% endif %}
<th style="width: 50px; padding: 10px;">
<button type="button" class="export-btn btn" style="width: 50px;">Export</button>
<button type="button" class="export-btn btn" style="width: 50px;">{{ _("Export") }}</button>
</th>
</tr>
</thead>
<tr class="empty">
<td></td>
<td>Fetching addresses ...</td>
<td>{{ _("Fetching addresses...") }}</td>
<td class="optional"></td>
<td class="optional"></td>
<td class="optional"></td>
@ -402,11 +402,11 @@
async function fetchAddressesItems(self) {
self.callId++;
// Clean up from existing addresses list
self.emptyRow.children[1].innerText = "Fetching addresses...";
self.emptyRow.children[1].innerText = '{{ _("Fetching addresses...") }}';
self.emptyRow.classList.remove('hidden');
self.tbody.querySelectorAll('address-row').forEach((row) => { row.remove() });
if (!self.listType) {
self.emptyRow.children[1].innerText = "Failed to fetch addresses...";
self.emptyRow.children[1].innerText = '{{ _("Failed to fetch addresses...") }}';
return
}
@ -416,9 +416,9 @@
self.export.href += `?onlyCurrentType=${self.onlyCurrentTypeSwitch.checked}&addressType=${self.listType}&sortby=${self.sortby}&sortdir=${self.sortdir}`;
if (self.listType == "receive") {
self.switchText.innerText = "Only receive addresses: "
self.switchText.innerText = '{{ _("Only receive addresses") }}: '
} else if (self.listType == "change") {
self.switchText.innerText = "Only change addresses: "
self.switchText.innerText = '{{ _("Only change addresses") }}: '
} else {
self.switchText.innerText = "";
}
@ -478,7 +478,7 @@
// Show empty row if no addresses found
if (self.pageCount == 0 || addressesList.length == 0) {
self.emptyRow.classList.remove('hidden');
self.emptyRow.children[1].innerText = "No addresses found...";
self.emptyRow.children[1].innerText = '{{ _("No addresses found...") }}';
self.pageCount = 1;
} else {
self.emptyRow.classList.add('hidden');
@ -517,10 +517,10 @@
}
return;
}
showError('Failed to fetch addresses list.')
showError('{{ _("Failed to fetch addresses list") }}')
} catch (e) {
console.log("Caught error: ", e);
showError(`Failed to fetch addresses list: ${e}`);
showError(`{{ _("Failed to fetch addresses list") }}: ${e}`);
}
}
@ -547,7 +547,7 @@
</address-data>`;
showPageOverlay('address-popup');
} else {
copyText(address, `Copied address: ${ address }`);
copyText(address, `{{ _("Copied address") }}: ${ address }`);
}
}

View file

@ -1,5 +1,5 @@
<div id="hwi_passphrase" class="hidden">
<h2>Enter Passphrase</h2>
<h2>{{ _("Enter Passphrase") }}</h2>
<div class="flex-center flex-column">
<br/>
<div>
@ -19,7 +19,7 @@
let inp = document.getElementById("hwi_passphrase_passphrase");
let submit = document.createElement('button');
submit.classList.add('btn');
submit.innerHTML = "Submit";
submit.innerHTML = '{{ _("Submit") }}';
let result = null;
submit.addEventListener('click', e=>{
result = inp.value;

View file

@ -1,5 +1,5 @@
<div id="hwi_pin" class="hidden">
<h2>Enter PIN</h2>
<h2>{{ _("Enter PIN") }}</h2>
<div class="row">
<div id="hwi_pin_stars" style="margin-top: 20px; font-size: 20px; height: 25px; width: 100%; padding-left: 30px;"></div>
<img id="hwi_pin_delete" src="{{ url_for('static', filename='img/clear-character.svg') }}" width="24px" style="margin-top: 20px; height: 25px; margin-left: 10px; cursor: pointer;" class="svg-white">
@ -13,7 +13,7 @@
async function enterPin(device, promptPin=true){
let result = null;
if (promptPin) {
showHWIProgress("Processing...", `Keep your ${capitalize(device.type)} connected.`);
showHWIProgress('{{ _("Processing...") }}', `{{ _("Keep your") }} ${capitalize(device.type)} {{ _("connected") }}.`);
try {
result = await hwi.promptPin(device);
} catch (error) {
@ -59,7 +59,7 @@
el.appendChild(document.createElement('br'));
let submit = document.createElement('button');
submit.classList.add('btn');
submit.innerHTML = "Submit";
submit.innerHTML = '{{ _("Submit") }}';
submit.addEventListener('click', e=>{
pinReady = true;
});
@ -81,7 +81,7 @@
}
hidePageOverlay();
// now we have PIN - send it
showHWIProgress("Processing...", `Keep your ${capitalize(device.type)} connected.`);
showHWIProgress('{{ _("Processing...") }}', `{{ _("Keep your") }} ${capitalize(device.type)} {{ _("connected") }}.`);
result = null;
try {
result = await hwi.sendPin(device, pin);

View file

@ -1,5 +1,5 @@
<div id="hwi_selector" class="hidden">
<h1>Select USB Wallet</h1>
<h1>{{ _("Select USB Wallet") }}</h1>
<div class="flex-center flex-column" id="hwi_selector_container">
</div>
</div>
@ -50,7 +50,7 @@
return null;
}
showHWIProgress("Processing...", `Keep your ${capitalize(selectedDevice.type)} connected.`);
showHWIProgress('{{ _("Processing...") }}', `{{ _("Keep your") }} ${capitalize(selectedDevice.type)} {{ _("connected") }}.`);
// Get the device again as it is now unlocked and the fingerprint is available
let rescanDevices = await enumerate(selectedDevice.type, selectedDevice.path, passphrase);
if (rescanDevices.length == 1) {
@ -75,7 +75,7 @@
return null;
}
if(!('success' in result) || !result.success){
throw "Failed to unlock device! Invalid PIN code?";
throw '{{ _("Failed to unlock device! Invalid PIN code?") }}';
}
device.needs_pin_sent = false;
}
@ -99,7 +99,7 @@
return;
}
let device = await selectDevice(devices, '');
showHWIProgress("Processing...", `Keep your ${capitalize(device.type)} connected.`);
showHWIProgress('{{ _("Processing...") }}', `{{ _("Keep your") }} ${capitalize(device.type)} {{ _("connected") }}.`);
let result = null;
try {
result = await hwi.togglePassphrase(device);
@ -122,7 +122,7 @@
throw "Failed to unlock device! Invalid PIN code?";
}
}
showNotification(device.type + " passphrase toggled successfuly", 5000)
showNotification(device.type + ' {{ _("passphrase toggled successfully") }}', 5000)
}
async function signMessage(deviceType, message, derivationPath, fingerprint=null){
@ -137,10 +137,10 @@
}
if (fingerprint && device.fingerprint != fingerprint) {
handleHWIError("Selected device does not have matching signing key.");
handleHWIError('{{ _("Selected device does not have matching signing key.") }}');
return;
}
showHWIProgress("Signing message...", `Confirm on your ${capitalize(device.type)}.`);
showHWIProgress('{{ _("Signing message...") }}', `{{ _("Confirm on your") }} ${capitalize(device.type)}.`);
let signature = null;
try {
signature = await hwi.signMessage(device, message, derivationPath);

View file

@ -1,10 +1,10 @@
{% if wallet is defined %}
<div id="hwi_display_address" class="flex-center flex-column hidden" style="overflow-wrap: break-word;">
<h2>Confirm Address</h2><br>
<h2>{{ _("Confirm Address") }}</h2><br>
<div>
Please confirm address matches on your <span id="hwi_device_name">device</span><br><br>
Expected address:<br><span id="expected_address"></span>
{{ _("Please confirm address matches on your <span id=\"hwi_device_name\">device</span>") }}<br><br>
{{ _("Expected address") }}:<br><span id="expected_address"></span>
</div>
<div class="flex-center">
<img src="{{ url_for('static', filename='img/loader_boxes.svg') }}"/>
@ -35,10 +35,10 @@
}
if (fingerprint && device.fingerprint != fingerprint) {
handleHWIError("Selected device does not have matching signing key.");
handleHWIError('{{ _("Selected device does not have matching signing key.") }}');
return;
}
showHWIProgress("Signing transaction...", `Confirm on your ${capitalize(device.type)}.`);
showHWIProgress('{{ _("Signing transaction...") }}', `{{ _("Confirm on your") }} ${capitalize(device.type)}.`);
let signedTx = null;
try {
signedTx = await hwi.signTx(device, psbt);
@ -87,12 +87,12 @@
(("{{ specter.chain }}" != "regtest" || !result.startsWith("tb")) ||
address.slice(0, -6) != "bcrt" + result.slice(2, -6))
) {
showError("Addresses did not match! Be careful! Please contact support and beware of using the displayed address!");
showError('{{ _("Addresses did not match! Be careful! Please contact support and beware of using the displayed address!") }}');
} else {
if (device.type == 'coldcard') {
showNotification("Address displayed successfully! Please check the device screen.", 7000);
showNotification('{{ _("Address displayed successfully! Please check the device screen.") }}', 7000);
} else {
showNotification("Address was verified successfully!", 5000);
showNotification('{{ _("Address was verified successfully!") }}', 5000);
}
}
}

View file

@ -11,26 +11,26 @@
<div id="hwi_progress" class="hidden" style="width: 270px;">
<h1 id="hwi_progress_title">Processing...</h1>
<div class="flex-center" id="hwi_progress_text">Keep your wallet connected.</div><br>
<h1 id="hwi_progress_title">{{ _("Processing...") }}</h1>
<div class="flex-center" id="hwi_progress_text">{{ _("Keep your wallet connected.") }}</div><br>
<p id="bitbox02-pairing-code" style="margin: auto;"></p>
<img src="{{ url_for('static', filename='img/loader_boxes.svg') }}">
<div class="flex-center hidden" id="hwi_progress_note" style="cursor: pointer;">
If your device does not show up, ensure the Specter USB settings are correctly configured.<br><br>
{{ _("If your device does not show up, ensure the Specter USB settings are correctly configured.") }}<br><br>
<a href="{{ url_for('settings_endpoint.hwi') }}" style="color: #fff; text-decoration: underline; cursor: pointer;">
View USB settings
{{ _("View USB settings") }}
</a>
</div>
<br>
</div>
<div class="flex-column hidden" id="coldcard_warning">
<p> Do you really want to use your Coldcard over USB (no airgap)? </p>
<p> {{ _("Do you really want to use your Coldcard over USB (no airgap)?") }} </p>
<div class="flex-center">
<button onclick="coldCardNotice()" class="btn flex-center"> Yes, I am living on the edge! </button>
<button onclick="coldCardNotice()" class="btn flex-center"> {{ _("Yes, I am living on the edge!") }} </button>
</div> <br>
<div class="flex-center">
<button onclick = "goToCoinkite()" class="btn flex-center"> No, I'll use the address explorer on the Coldcard. Get me to the coinkite site for more info. </button>
<button onclick = "goToCoinkite()" class="btn flex-center"> {{ _("No, I'll use the address explorer on the Coldcard. Get me to the coinkite site for more info.") }} </button>
</div>
</div>
@ -47,7 +47,7 @@
<script type="text/javascript">
function showHWIProgress(title="Processing...", text="Keep your wallet connected."){
function showHWIProgress(title='{{ _("Processing...") }}', text='{{ _("Keep your wallet connected.") }}'){
document.getElementById('hwi_progress_title').innerHTML = title;
document.getElementById('hwi_progress_text').innerHTML = text;
showPageOverlay('hwi_progress');
@ -82,7 +82,7 @@
}
// detect device
if (devicePath == null) {
showHWIProgress("Detecting...", "Plug in your device");
showHWIProgress('{{ _("Detecting...") }}', '{{ _("Plug in your device") }}');
}
let result = [];
let retryCounter = 0;
@ -97,7 +97,7 @@
// Likely bitbox02 pairing
let pairingCode = (await hwi.getBitbox02PairingCode()).code;
if (pairingCode) {
document.getElementById('bitbox02-pairing-code').innerHTML = `Pairing Code:<br><br><b>${pairingCode}</b><br><br>Please confirm the code matches the one displayed on your BitBox02 device.<br><br>It could take up to 20 seconds to continue after approving the pairing on the device, please be patient.`;
document.getElementById('bitbox02-pairing-code').innerHTML = `{{ _("Pairing Code") }}:<br><br><b>${pairingCode}</b><br><br>{{ _("Please confirm the code matches the one displayed on your BitBox02 device.<br><br>It could take up to 20 seconds to continue after approving the pairing on the device, please be patient.") }}`;
}
}
}
@ -126,7 +126,7 @@
// Likely bitbox02 pairing
let pairingCode = (await hwi.getBitbox02PairingCode()).code;
if (pairingCode) {
document.getElementById('bitbox02-pairing-code').innerHTML = `Pairing Code:<br><br><b>${pairingCode}</b><br><br>Please confirm the code matches the one displayed on your BitBox02 device.<br><br>It could take up to 20 seconds to continue after approving the pairing on the device, please be patient.`;
document.getElementById('bitbox02-pairing-code').innerHTML = `{{ _("Pairing Code") }}:<br><br><b>${pairingCode}</b><br><br>{{ _("Please confirm the code matches the one displayed on your BitBox02 device.<br><br>It could take up to 20 seconds to continue after approving the pairing on the device, please be patient.") }}`;
}
}
}
@ -160,8 +160,8 @@
async function getXpubs(device, account=0, passphrase=""){
// detect device
showHWIProgress("Extracting public keys...",
`It may take a while.<br><br>Keep your ${capitalize(device.type)} connected.`);
showHWIProgress('{{ _("Extracting public keys...") }}',
`{{ _("It may take a while.") }}<br><br>{{ _("Keep your") }} ${capitalize(device.type)} {{ _("connected") }}.`);
let result = [];
try {
result = await hwi.getXpubs(device, account, passphrase);
@ -180,8 +180,8 @@
async function getXpub(device, derivation="", passphrase="", chain=""){
// detect device
showHWIProgress("Extracting public key...<br><span style='font-size: 0.8em;'>Derivation: " + derivation + "</span>",
`It may take a while.<br><br>Keep your ${capitalize(device.type)} connected.`);
showHWIProgress('{{ _("Extracting public key...") }}<br><span style=\'font-size: 0.8em;\'>{{ _("Derivation") }}: ' + derivation + "</span>",
`{{ _("It may take a while.") }}<br><br>{{ _("Keep your") }} ${capitalize(device.type)} {{ _("connected") }}.`);
let result = [];
try {
result = await hwi.getXpub(device, derivation, passphrase, chain);
@ -200,8 +200,8 @@
async function getMasterBlindingKey(device, passphrase="", chain=""){
// detect device
showHWIProgress("Extracting master blinding key...",
`Please confirm action on your ${capitalize(device.type)} device.`);
showHWIProgress('{{ _("Extracting master blinding key...") }}',
`{{ _("Please confirm action on your") }} ${capitalize(device.type)} {{ _("device") }}.`);
let result = [];
try {
result = await hwi.getMasterBlindingKey(device, passphrase, chain);

View file

@ -0,0 +1,33 @@
function changeLanguage(selectObj) {
// get the index of the selected option
var idx = selectObj.selectedIndex;
// get the value of the selected option
var language_code = selectObj.options[idx].value;
fetch(
"{{ url_for('set_language_code') }}",
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({language_code: language_code})
}
)
.then(response => response.json())
.then(jsonResponse => {
if (jsonResponse.success) {
location.reload();
} else {
showError('{{ _("Failed to change language") }}')
}
return;
})
.catch(err => {
console.log("Caught error: ", err);
showError(err);
});
}

View file

@ -0,0 +1,8 @@
{#
Relies on `supported_languages=app.supported_languages` being injected into the context
#}
<select name="language_code" onchange="changeLanguage(this)">
{% for language_code, language in supported_languages.items() %}
<option value="{{ language_code }}" {% if session["language_code"] == language_code %}selected{% endif %}>{{ language }}</option>
{% endfor %}
</select>

View file

@ -1,5 +1,5 @@
<tool-tip title="Merkle Proof Validation" style="float: right;">
Specter-Desktop validates <a href="https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki" target="_blank">BIP 37</a> Merkle Proofs from your full node <strong>guaranteeing</strong> that transactions displayed as confirmed have been included in the specified block hash.<br><br>
However, a compromised full node could return a fake block that was never mined into Bitcoin's blockchain!
If you find the displayed block hash in other locations (other nodes, block explorers, etc) <strong>and you trust specter-desktop isn't lying to you</strong>, then you can be sure this transaction exists in their blockchain as well.
{{ _('Specter-Desktop validates <a href="https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki" target="_blank">BIP 37</a> Merkle Proofs from your full node <strong>guaranteeing</strong> that transactions displayed as confirmed have been included in the specified block hash.') }}<br><br>
{{ _("However, a compromised full node could return a fake block that was never mined into Bitcoin's blockchain!") }}
{{ _("If you find the displayed block hash in other locations (other nodes, block explorers, etc) <strong>and you trust specter-desktop isn't lying to you</strong>, then you can be sure this transaction exists in their blockchain as well.") }}"
</tool-tip>

View file

@ -1,14 +1,14 @@
<div id="tx_broadcast" class="hidden">
<h1>Transaction is Ready to Broadcast</h1>
<h1>{{ _("Transaction is Ready to Broadcast") }}</h1>
<div class="flex-center flex-column">
<button id="broadcast_local_btn" class="btn flex-item" style="width: 190px;">Send transaction</button>
<button type="button" id="broadcast_block_explorer_btn" class="btn flex-item {% if specter.chain == 'regtest' %}hidden{% endif %}" style="width: 190px;">Send via block explorer</button>
<button type="button" id="broadcast_blockstream_btn" class="btn flex-item hidden" style="width: 190px;">Send via blockstream.info</button>
<button type="button" id="broadcast_mempool_btn" class="btn flex-item hidden" style="width: 190px;">Send via mempool.space</button>
<button type="button" id="broadcast_blockstream_tor_btn" class="btn flex-item hidden" style="width: 190px;">Send via blockstream.info<br>(over Tor hidden service)</button>
<button type="button" id="broadcast_mempool_tor_btn" class="btn flex-item hidden" style="width: 190px;">Send via mempool.space<br>(over Tor hidden service)</button>
<button type="button" id="broadcast_options_btn" class="btn flex-item hidden" style="width: 190px;">Back to options</button>
<button id="save_psbt_btn" class="btn flex-item" style="width: 190px;">Save as pending</button>
<button id="copy_final_tx_btn" class="btn flex-item" style="width: 190px;">Copy raw transaction</button>
<button id="broadcast_local_btn" class="btn flex-item" style="width: 190px;">{{ _("Send transaction") }}</button>
<button type="button" id="broadcast_block_explorer_btn" class="btn flex-item {% if specter.chain == 'regtest' %}hidden{% endif %}" style="width: 190px;">{{ _("Send via block explorer") }}</button>
<button type="button" id="broadcast_blockstream_btn" class="btn flex-item hidden" style="width: 190px;">{{ _("Send via blockstream.info") }}</button>
<button type="button" id="broadcast_mempool_btn" class="btn flex-item hidden" style="width: 190px;">{{ _("Send via mempool.space") }}</button>
<button type="button" id="broadcast_blockstream_tor_btn" class="btn flex-item hidden" style="width: 190px;">{{ _("Send via blockstream.info") }}<br>({{ _("over Tor hidden service") }}</button>
<button type="button" id="broadcast_mempool_tor_btn" class="btn flex-item hidden" style="width: 190px;">{{ _("Send via mempool.space") }}<br>({{ _("over Tor hidden service") }}</button>
<button type="button" id="broadcast_options_btn" class="btn flex-item hidden" style="width: 190px;">{{ _("Back to options") }}</button>
<button id="save_psbt_btn" class="btn flex-item" style="width: 190px;">{{ _("Save as pending") }}</button>
<button id="copy_final_tx_btn" class="btn flex-item" style="width: 190px;">{{ _("Copy raw transaction") }}</button>
</div>
</div>

View file

@ -14,13 +14,13 @@
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<div>
<span id='key_qr_code'></span>
<br><pre id="key_text" class='key center' onclick="copyText(this.innerText, 'Coppied full key')">{{ key }}</pre>
<br><pre id="key_text" class='key center' onclick="copyText(this.innerText, '{{ _("Copied full key") }}')">{{ key }}</pre>
</div>
<br>
<table style="text-align: left; width: 70%; margin: auto; border: none;">
<tr style="background: unset; border: none;">
<td style="border: none;">
<label class="switch-label">Show derivation path:&nbsp;</label>
<label class="switch-label">{{ _("Show derivation path") }}:&nbsp;</label>
</td>
<td style="border: none;">
<label class="switch">
@ -31,7 +31,7 @@
</tr>
<tr style="background: unset; border: none;">
<td style="border: none;">
<label class="switch-label">Use SLIP-132:&nbsp;</label>
<label class="switch-label">{{ _("Use SLIP-132") }}:&nbsp;</label>
</td>
<td style="border: none;">
<label class="switch">

View file

@ -2,7 +2,7 @@
<div class="page_overlay_popup" id="page_overlay_popup">
<div id="page_overlay_popup_content"></div>
<div class="page_overlay_popup_cancel">
<a href="#" id="page_overlay_popup_cancel_button" class="item medium" onclick="cancelOverlay()">cancel</a>
<a href="#" id="page_overlay_popup_cancel_button" class="item medium" onclick="cancelOverlay()">{{ _("cancel") }}</a>
</div>
<br/>
<div class="clear-both"></div>
@ -11,7 +11,7 @@
<div class="loader" id="pacman">
<img src="{{ url_for('static', filename='img/loader.gif') }}"/>
<div>Loading... It may take a while...</div>
<div>{{ _("Loading... It may take a while...") }}</div>
</div>
<script>

View file

@ -1,6 +1,7 @@
<form id="{{ device.alias }}_sign_tx_qr" class="flex-center flex-column hidden">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<h2>Scan this QR code with your {{device.name}}.</h2>
<h2>{{ _("Scan this QR code with your") }} {{device.name}}.</h2>
<div class="row" style="min-height: 400px; margin-top:20px">
<span id="compressed_psbt" 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>
@ -8,7 +9,7 @@
</div>
<qr-scanner id="{{ device.alias }}_qr_scan_btn">
<a slot="button" class="btn flex-item" style="width: 250px; margin: 15px auto;">
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 30px; margin: 0px;" class="svg-white"> Scan signed transaction
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 30px; margin: 0px;" class="svg-white"> {{ _("Scan signed transaction") }}
</a>
</qr-scanner>
@ -25,7 +26,7 @@
hidePageOverlay();
combine(result);
} else {
showError("Scan failed, please try again...", 3000);
showError('{{ _("Scan failed, please try again...") }}', 3000);
}
});
</script>

View file

@ -1,8 +1,8 @@
<div id="{{ device.alias }}_sign_tx_file" class="flex-center flex-column hidden">
<h2>Sign Transaction with file</h2><br>
<a class="btn centered" style="margin: 15px auto;" download="to{{ psbt['address'] }}{{ psbt['amount'] }}.psbt" href="data:application/octet-stream;base64;content-disposition=attachment,{{ device_psbts['sdcard'] }}">Save transaction to file</a>
<h2>{{ _("Sign Transaction with file") }}</h2><br>
<a class="btn centered" style="margin: 15px auto;" download="to{{ psbt['address'] }}{{ psbt['amount'] }}.psbt" href="data:application/octet-stream;base64;content-disposition=attachment,{{ device_psbts['sdcard'] }}">{{ _("Save transaction to file") }}</a>
<input type="file" id="{{ device.alias }}_signing_file" class="inputfile"/>
<label for="{{ device.alias }}_signing_file" class="btn centered" style="margin: 15px auto;">Load signed transaction from file</label>
<label for="{{ device.alias }}_signing_file" class="btn centered" style="margin: 15px auto;">{{ _("Load signed transaction from file") }}</label>
</div>
<script>

View file

@ -9,13 +9,13 @@
{% macro sign_tx_method(wallet, device, psbt, chain) -%}
{% set device_psbts = device.create_psbts(psbt['base64'], wallet) %}
<div id="{{ device.alias }}_sign_tx_method" class="flex-center flex-column hidden">
<h2>Sign Transaction</h2><br>
<h2>{{ _("Sign Transaction") }}</h2><br>
<div>
Please choose a signing method for your {{ device.name }}.
{{ _("Please choose a signing method for your") }} {{ device.name }}.
</div>
{% if device.hwi_support %}
<button id="{{ device.alias }}_hwi_sign_btn" class="btn flex-item" style="width: 190px; margin: 15px auto;">
<img src="{{ url_for('static', filename='img/usb.svg') }}" style="width: 26px; margin-right: 2px;" class="svg-white">Sign via USB (HWI)
<img src="{{ url_for('static', filename='img/usb.svg') }}" style="width: 26px; margin-right: 2px;" class="svg-white">{{ _("Sign via USB (HWI)") }}
</button>
<script>
document.getElementById("{{ device.alias }}_hwi_sign_btn").addEventListener("click", e => {
@ -37,7 +37,7 @@
{% if device.sd_card_support %}
{% include "includes/overlay/sd_card_sign.jinja" %}
<button id="{{ device.alias }}_file_sign_btn" class="btn flex-item" style="width: 190px; margin: 15px auto;">
<img src="{{ url_for('static', filename='img/sd-card.svg') }}" style="width: 26px; margin: 0px;" class="svg-white"> Sign with SD Card file
<img src="{{ url_for('static', filename='img/sd-card.svg') }}" style="width: 26px; margin: 0px;" class="svg-white"> {{ _("Sign with SD Card file") }}
</button>
<script>
document.getElementById("{{ device.alias }}_file_sign_btn").addEventListener("click", e => {
@ -50,7 +50,7 @@
{% if device.qr_code_support %}
{% include "includes/overlay/qr_code_sign.jinja" %}
<button id="{{ device.alias }}_qr_sign_btn" class="btn flex-item" style="width: 190px; margin: 15px auto;">
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 26px; margin: 0px;" class="svg-white"> Sign with QR codes
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 26px; margin: 0px;" class="svg-white"> {{ _("Sign with QR codes") }}
</button>
<script>
document.getElementById("{{ device.alias }}_qr_sign_btn").addEventListener("click", e => {
@ -64,10 +64,10 @@
<button id="{{ device.alias }}_hot_sign_btn" class="btn flex-item" style="width: 190px; margin: 15px auto;">
{% if device.device_type == "bitcoincore" %}
{% from "components/bitcoin_svg.jinja" import bitcoin_svg %}
{{ bitcoin_svg(chain, 16) }} Sign with Bitcoin Core
{{ bitcoin_svg(chain, 16) }} {{ _("Sign with Bitcoin Core") }}
{% else %}
{% from "components/liquid_svg.jinja" import liquid_svg %}
{{ liquid_svg(chain, 16) }} Sign with Elements Core
{{ liquid_svg(chain, 16) }} {{ _("Sign with Elements Core") }}
{% endif %}
</button>
<script>
@ -80,9 +80,9 @@
<div id="hot_enter_passphrase" class="hidden">
{% set is_encrypted = device.is_encrypted(wallet.manager) %}
{% if is_encrypted %}
<h2>Enter decryption password</h2>
<h2>{{ _("Enter decryption password") }}</h2>
{% else %}
<h2>Proceed with signing</h2>
<h2>{{ _("Proceed with signing") }}</h2>
{% endif %}
<form action="{{ url_for('wallets_endpoint.send_new', wallet_alias=wallet.alias) }}" method="POST" id="hot_enter_passphrase__content" class="flex-center flex-column">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
@ -91,7 +91,7 @@
{% if is_encrypted %}
<input name="passphrase" type="password" id="hwi_enter_passphrase__passphrase" />
{% else %}
Wallet is not encrypted, just click the button.
{{ _("Wallet is not encrypted, just click the button.") }}
<input name="passphrase" type="hidden" id="hwi_enter_passphrase__passphrase" value="" />
{% endif %}
<input name="device" type="hidden" value="{{ device.alias }}" />
@ -99,7 +99,7 @@
</div>
<br/>
<br/>
<button id="hot_enter_passphrase__submit" class="btn" type="submit" name="action" value="signhotwallet">Sign transaction</button>
<button id="hot_enter_passphrase__submit" class="btn" type="submit" name="action" value="signhotwallet">{{ _("Sign transaction") }}</button>
</form>
</div>
{% endif %}

View file

@ -12,14 +12,14 @@
min-width: 5em;
}
</style>
<span class="page-limit-label">Page limit: </span>
<span class="page-limit-label">{{ _("Page limit") }}: </span>
<fieldset class="page-limit-fieldset">
<select class="page-limit-select">
<option value="10">10</option>
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
<option value="0">All</option>
<option value="0">{{ _("All") }}</option>
</select>
</fieldset>
</div>

View file

@ -204,7 +204,7 @@ class QRCodeElement extends HTMLElement {
value = this.maybe_remove_checksum(value);
this.qrcode.makeCode(value);
if(this.isQRlarge){
this.note.innerHTML = "Click to animate";
this.note.innerHTML = '{{ _("Click to animate") }}';
}
}
});

View file

@ -125,7 +125,7 @@ class QRScannerElement extends HTMLElement {
return this.triggerError("Invalid QR code! n > m!");
}
if(this.partial_qr.len != 0 && this.partial_qr.len != n){
return this.triggerError("Number of frames in QR code mismatch!<br>Had "+this.partial_qr.len+", got "+n);
return this.triggerError('{{ _("Number of frames in QR code mismatch!") }}<br>Had ' + this.partial_qr.len + ", got " + n);
}
// if it is the first qr code - build array
if(this.partial_qr.len == 0){
@ -199,14 +199,14 @@ class QRScannerElement extends HTMLElement {
let isopen = this.hasAttribute('open');
if(isopen){
if(navigator.mediaDevices === undefined && location.protocol == "http:"){
this.triggerError("Can't get access to the camera. Specter should run on localhost or over https to enable QR code scanning.");
this.triggerError("{{ _("Can\'t get access to the camera. Specter should run on localhost or over https to enable QR code scanning.") }}");
return;
}
try{
this.popup.style.display = 'flex';
this.scanner.start();
}catch(e){
this.triggerError("Can't start the QR scanner!<br>" + e);
this.triggerError("{{ _("Can\'t start the QR scanner!") }}<br>" + e);
}
}else{
this.reset();

View file

@ -1,41 +1,40 @@
<div id="bitcoin_core_info" class="hidden" style="text-align: left; max-width: 500px;">
<h1>Bitcoin Core Node Info:</h1>
<h1>{{ _("Bitcoin Core Node Info") }}:</h1>
{% if specter.chain %}
{% if specter.info['pruned'] %}
<p class="warning"><img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>You are using a pruned node.<br>Pruned nodes may not be able to scan for old balances when importing an existing wallet.<br>It is recommended to use Specter with an unpruned node.</p>
<p class="warning"><img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>{{ _("You are using a pruned node.<br>Pruned nodes may not be able to scan for old balances when importing an existing wallet.<br>It is recommended to use Specter with an unpruned node.") }}</p>
<br>
{% elif not specter.info['blockfilterindex'] %}
<p class="warning"><img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>Your node does not have <code>blockfilterindex</code> enabled.<br>Setting <code>blockfilterindex=1</code> in your <code>bitcoin.conf</code> file is recommended as it takes just a few GB of storage<br>and helps to speed-up blockchain rescanning.</p>
<p class="warning"><img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>{{ _("Your node does not have <code>blockfilterindex</code> enabled.<br>Setting <code>blockfilterindex=1</code> in your <code>bitcoin.conf</code> file is recommended as it takes just a few GB of storage<br>and helps to speed-up blockchain rescanning.") }}</p>
<br>
{% endif %}
<table>
<tr> <td style="text-align: left;">Network:</td> <td style="text-align: right;" id="node-info-specter-chain">{{specter.chain}}</td> </tr>
<tr> <td style="text-align: left; width: 35%;">Bitcoin Core Version:</td> <td style="text-align: right;">v{{specter.bitcoin_core_version}} <span class="note">({{specter.network_info['version']}})</span></td> </tr>
<tr> <td style="text-align: left;">Connections count:</td> <td style="text-align: right;">{{specter.network_info['connections']}}</td> </tr>
<tr> <td style="text-align: left;">Difficulty:</td> <td style="text-align: right;">{{specter.info.get('difficulty', 0) | int}}</td> </tr>
<tr> <td style="text-align: left;">Size on disk:</td> <td style="text-align: right;">{{specter.info['size_on_disk']|bytessize }}</td> </tr>
<tr> <td style="text-align: left;">Blocks count:</td> <td style="text-align: right;">{{specter.info['blocks']}}</td> </tr>
<tr> <td style="text-align: left;">Last block hash:</td> <td style="text-align: right"><code style="word-break: break-word;">{{specter.info['bestblockhash']}}</code></td> </tr>
<tr> <td style="text-align: left;">Mempool Size:</td> <td style="text-align: right;">{{specter.info['mempool_info'].size}} transactions</td> </tr>
<tr> <td style="text-align: left;">Node uptime:</td> <td style="text-align: right;">~ {{(specter.info['uptime'] / 60 // 60) | int }} Hours</td> </tr>
<tr> <td style="text-align: left;">{{ _("Network") }}:</td> <td style="text-align: right;" id="node-info-specter-chain">{{specter.chain}}</td> </tr>
<tr> <td style="text-align: left; width: 35%;">{{ _("Bitcoin Core Version") }}:</td> <td style="text-align: right;">v{{specter.bitcoin_core_version}} <span class="note">({{specter.network_info['version']}})</span></td> </tr>
<tr> <td style="text-align: left;">{{ _("Connections count") }}:</td> <td style="text-align: right;">{{specter.network_info['connections']}}</td> </tr>
<tr> <td style="text-align: left;">{{ _("Difficulty") }}:</td> <td style="text-align: right;">{{specter.info.get('difficulty', 0) | int}}</td> </tr>
<tr> <td style="text-align: left;">{{ _("Size on disk") }}:</td> <td style="text-align: right;">{{specter.info['size_on_disk']|bytessize }}</td> </tr>
<tr> <td style="text-align: left;">{{ _("Blocks count") }}:</td> <td style="text-align: right;">{{specter.info['blocks']}}</td> </tr>
<tr> <td style="text-align: left;">{{ _("Last block hash") }}:</td> <td style="text-align: right"><code style="word-break: break-word;">{{specter.info['bestblockhash']}}</code></td> </tr>
<tr> <td style="text-align: left;">{{ _("Mempool Size") }}:</td> <td style="text-align: right;">{{specter.info['mempool_info'].size}} transactions</td> </tr>
<tr> <td style="text-align: left;">{{ _("Node uptime") }}:</td> <td style="text-align: right;">~ {{(specter.info['uptime'] / 60 // 60) | int }} {{ _("Hours") }}</td> </tr>
{% if specter.info['pruned'] %}
<tr> <td style="text-align: left;">Automatic pruning:</td> <td style="text-align: right;">{{specter.info['automatic_pruning']}}</td> </tr>
<tr> <td style="text-align: left;">Prune height:</td> <td style="text-align: right;">{{specter.info['pruneheight']}}</td> </tr>
<tr> <td style="text-align: left;">Prune target size:</td> <td style="text-align: right;">{{specter.info['prune_target_size']}}</td> </tr>
<tr> <td style="text-align: left;">{{ _("Automatic pruning") }}:</td> <td style="text-align: right;">{{specter.info['automatic_pruning']}}</td> </tr>
<tr> <td style="text-align: left;">{{ _("Prune height") }}:</td> <td style="text-align: right;">{{specter.info['pruneheight']}}</td> </tr>
<tr> <td style="text-align: left;">{{ _("Prune target size") }}:</td> <td style="text-align: right;">{{specter.info['prune_target_size']}}</td> </tr>
{% endif %}
</table>
<p id="total_supply" style="line-height: 2.5;"></p>
<div class="row">
<button type="button" onclick="fetchTotalSupply()" class="btn centered">
Run the numbers!
{{ _("Run the numbers!") }}
<div class="tool-tip">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Calculate the total Bitcoin supply<br><br></span>
This will run Bitcoin Core's <code>gettxoutsetinfo</code> command,
which will calculate the total amount of Bitcoin's UTXO set.<br><br>
This might take a few minutes...<br>
<span class="info__title">{{ _("Calculate the total Bitcoin supply") }}<br><br></span>
{{ _("This will run Bitcoin Core's <code>gettxoutsetinfo</code> command which will calculate the total amount of Bitcoin's UTXO set.") }}<br><br>
{{ _("This might take a few minutes...") }}<br>
</span>
</p>
</div>
@ -44,7 +43,7 @@
<script>
let totalUserBalance = parseFloat(parseFloat("{{ specter.wallet_manager.wallets.values() | sum(attribute='fullbalance') }}").toFixed(8));
async function fetchTotalSupply() {
document.getElementById('total_supply').innerHTML = 'Running the numbers... (this might take a few minutes)';
document.getElementById('total_supply').innerHTML = '{{ _("Running the numbers... (this might take a few minutes)") }}';
try {
const response = await fetch(
"{{ url_for('txout_set_info') }}",
@ -55,9 +54,9 @@
let result = await response.json();
console.log(result)
let userBalanceFromTotal = parseFloat((100 / (result.total_amount / totalUserBalance)).toFixed(8));
document.getElementById('total_supply').innerHTML = `Bitcoin Total Supply: ${result.total_amount} BTC<br>` +
document.getElementById('total_supply').innerHTML = `{{ _("Bitcoin Total Supply") }}: ${result.total_amount} BTC<br>` +
( userBalanceFromTotal > 1e-8 ?
`<span class="note" style="margin: 7px auto;">Your wallets hold ${totalUserBalance} BTC (~${userBalanceFromTotal}% from the total supply)</span>` :
`<span class="note" style="margin: 7px auto;">{{ _("Your wallets hold") }} ${totalUserBalance} BTC (~${userBalanceFromTotal}% {{ _("from the total supply") }}</span>` :
''
)
} catch(e) {
@ -70,7 +69,7 @@
<div class="row">
<a id="active-node-settings-btn" class="btn centered" href="{{ url_for('nodes_endpoint.node_settings', node_alias=specter.node.alias) }}">
<img src="{{ url_for('static', filename='img/gear.svg') }}" style="width: 26px; margin: 0px;" class="svg-white">
Configure Bitcoin Core RPC Connection
{{ _("Configure Bitcoin Core RPC Connection") }}
</a>
</div>
{% endif %}

View file

@ -23,7 +23,7 @@
}
</style>
<div id="node_select_popup" class="hidden" style="text-align: left;">
<h1>Node configuration</h1>
<h1>{{ _("Node configuration") }}</h1>
{% for node_name in specter.node_manager.nodes_names %}
{% set node = specter.node_manager.nodes[node_name] %}
<form action="{{url_for('nodes_endpoint.switch_node')}}" id="{{node.alias}}-select-node-form" method="POST">
@ -45,7 +45,7 @@
{% if node.chain %}
{{node.chain | title}}, {{ node.host }}:{{ node.port }}
{% else %}
Node unreachable...
{{ _("Node unreachable...") }}
{% endif %}
</small>
</div>

View file

@ -9,7 +9,7 @@
<img src="{{ url_for('static', filename='img/devices/' ~ device.icon) }}" width="22px">
<div>
{{ device.name }}<br>
<span class="note" style="font-size: 0.75em;">{{ device.keys | length}} keys</span>
<span class="note" style="font-size: 0.75em;">{{ device.keys | length}} {{ _("keys") }}</span>
</div>
</a>
{%- endmacro %}

View file

@ -11,11 +11,11 @@
<img src="{{ url_for('static', filename='img/' + ('two-keys' if wallet.is_multisig else 'key') + '.svg') }}" class="svg-white" style="width: 45px; margin-left: 5px;">
<div class="grow">
{{ wallet.name }}<br>
<span class="note" style="font-size: 0.75em;">{% if wallet.is_multisig %}{{ wallet.sigs_required }} out of {{ wallet.keys_count }} Multisig{% else %}Single Key{% endif %}</span>
<span class="note" style="font-size: 0.75em;">{% if wallet.is_multisig %}{{ wallet.sigs_required }} out of {{ wallet.keys_count }} {{ _("Multisig") }}{% else %}{{ _("Single Key") }}{% endif %}</span>
{% if wallet.rescan_progress %}
<span id="{{ wallet.alias }}_sidebar_rescan_progress_container" style="display: contents;">
<br>
<span class="note" style="font-size: 0.7em;">Rescanning blockchain: <span id="{{ wallet.alias }}_sidebar_wallet_rescan_percents">{{ "%.2f"|format(wallet.rescan_progress * 100) }}</span>%</span>
<span class="note" style="font-size: 0.7em;">{{ _("Rescanning blockchain") }}: <span id="{{ wallet.alias }}_sidebar_wallet_rescan_percents">{{ "%.2f"|format(wallet.rescan_progress * 100) }}</span>%</span>
</span>
{% endif %}
</div>
@ -24,7 +24,7 @@
<img height="38px" class="spinning" spinning src="{{ url_for('static', filename='img/refresh.svg') }}" />
<div class="grow">
{{ wallet.name }}<br>
<span class="note" style="font-size: 0.75em;">Loading wallet...</span>
<span class="note" style="font-size: 0.75em;">{{ _("Loading wallet...") }}</span>
</div>
</a>
{% if wallet.rescan_progress %}

View file

@ -40,10 +40,10 @@
<div id="side-content">
<nav class="side">
{% if specter.info["initialblockdownload"] %}
<p class="warning" style="margin-top: 30px;"><img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>Bitcoin Core is still syncing...<br>(data might be outdated)</p>
<p class="warning" style="margin-top: 30px;"><img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>{{ _("Bitcoin Core is still syncing...<br>(data might be outdated)") }}</p>
{% endif %}
{% if specter.network_info.version < 200000 %}
<p class="warning" style="margin-top: 30px;"><img src="{{ url_for('static', filename='img/warning_sign.svg') }}" style="width: 20px;"/><br>Bitcoin Core version is outdated.<br>Some features might not work...<br>(minimum required: v20.0.0).</p>
<p class="warning" style="margin-top: 30px;"><img src="{{ url_for('static', filename='img/warning_sign.svg') }}" style="width: 20px;"/><br>{{ _("Bitcoin Core version is outdated.<br>Some features might not work...<br>(minimum required: v20.0.0).") }}</p>
{% endif %}
<div
id="active-node" onclick="showPageOverlay('bitcoin_core_info');document.getElementById('side-content').classList.remove('active');"
@ -73,7 +73,7 @@
{{ specter.info.chain }}
{% endif %}
{% else %}
Unavailable. Click to configure.
{{ _("Unavailable. Click to configure.") }}
{% endif %}
</small>
</div>
@ -126,26 +126,26 @@
</script>
{% endif %}
{% if specter.setup_status.stage != "start" and specter.rpc is none %}
{{ sidebar_btn(url_for('setup_endpoint.' + specter.setup_status.stage), 'See setup progress', class='btn action', icon=false) }}
{{ sidebar_btn(url_for('setup_endpoint.' + specter.setup_status.stage), _('See setup progress'), class='btn action', icon=false) }}
{% endif %}
<div class="separator">
<span id="toggle_wallets_list" style="cursor: pointer;">Wallets &nbsp; &#9660;</span>
<span id="toggle_wallets_list" style="cursor: pointer;">{{ _("Wallets") }} &nbsp; &#9660;</span>
{% if (specter.wallet_manager.wallets | length) > 1 %}
<a class="note" id="wallets_overview_link" href="{{ url_for('wallets_endpoint.wallets_overview') }}" style="cursor: pointer;">
Wallets overview
{{ _("Wallets overview") }}
</a>
{% endif %}
</div>
<div id="wallets_list">
<span id="wallets-loading-indicator" class="note {% if not specter.wallet_manager.is_loading or not specter.chain %}hidden{% endif %}" style="text-align: left; margin-left: 20px; margin-top: 5px;">
<img height="16px" class="spinning" id="wallet-manager-loader" style="vertical-align: sub;" src="{{ url_for('static', filename='img/refresh.svg') }}" />
Updating wallets data...
{{ _("Updating wallets data...") }}
</span>
<span id="wallets-loading-done" class="note hidden" style="text-align: left; margin: 5px 5px 5px 20px;">
Finished updating your wallets
{{ _("Finished updating your wallets") }}
<br>
<span id="wallets-loading-done-refresh" class="explorer-link" onclick="location.reload()" style="text-decoration: underline; margin-left: 20px;">
Click here to refresh
{{ _("Click here to refresh") }}
</span>
</span>
{% set loading_wallets = [] %}
@ -159,29 +159,29 @@
{% endfor %}
{% endif %}
{% if specter.chain %}
{{ sidebar_btn(url_for('wallets_endpoint.new_wallet_type'), 'Add new wallet', 'btn_new_wallet') }}
{{ sidebar_btn(url_for('wallets_endpoint.new_wallet_type'), _('Add new wallet'), 'btn_new_wallet') }}
{% if specter.wallet_manager.failed_load_wallets %}
<form action="{{ url_for('wallets_endpoint.failed_wallets') }}" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<p class="warning center">
<img src="{{ url_for('static', filename='img/warning_sign.svg') }}" style="width: 20px;"/><br>Failed to load some wallets<br>
<button class="btn" name="action" value="retry_loading_wallets" style="margin: auto;">Retry loading wallets</button>
<button type="button" class="btn" onclick="showPageOverlay('failed_wallets_popup')" style="margin: auto; margin-top: 5px;">Show Details</button>
<img src="{{ url_for('static', filename='img/warning_sign.svg') }}" style="width: 20px;"/><br>{{ _("Failed to load some wallets") }}<br>
<button class="btn" name="action" value="retry_loading_wallets" style="margin: auto;">{{ _("Retry loading wallets") }}</button>
<button type="button" class="btn" onclick="showPageOverlay('failed_wallets_popup')" style="margin: auto; margin-top: 5px;">{{ _("Show Details") }}</button>
</p>
</form>
<div class="hidden" id="failed_wallets_popup">
{% for wallet in specter.wallet_manager.failed_load_wallets %}
<div class="card">
<h1>Wallet Name: {{wallet.name}}</h1>
<h2>Error Details:</h2>
<h1>{{ _("Wallet Name") }}: {{wallet.name}}</h1>
<h2>{{ _("Error Details") }}:</h2>
<p style="color: red">{{wallet.loading_error}}</p>
<br><br>
<form action="{{ url_for('wallets_endpoint.failed_wallets') }}" method="POST" class="row center">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="wallet_data" value='{{ wallet|tojson }}' />
<a class="btn" id="download_failed_wallet_{{wallet.alias}}" download="{{ wallet.name }}.json" style="width: 180px; height: 35px; margin-top: 1.5px;">Download wallet file</a>&nbsp;&nbsp;
<button class="btn" type="submit" name="action" value="importwallet" formaction="{{ url_for('wallets_endpoint.new_wallet', wallet_type='import_wallet') }}">Recreate the wallet</button>&nbsp;&nbsp;
<button class="btn" name="action" value="delete_failed_wallet">Delete wallet</button>
<a class="btn" id="download_failed_wallet_{{wallet.alias}}" download="{{ wallet.name }}.json" style="width: 180px; height: 35px; margin-top: 1.5px;">{{ _("Download wallet file") }}</a>&nbsp;&nbsp;
<button class="btn" type="submit" name="action" value="importwallet" formaction="{{ url_for('wallets_endpoint.new_wallet', wallet_type='import_wallet') }}">{{ _("Recreate the wallet") }}</button>&nbsp;&nbsp;
<button class="btn" name="action" value="delete_failed_wallet">{{ _("Delete wallet") }}</button>
</form>
</div>
<script>
@ -195,10 +195,10 @@
{% elif not specter.setup_status.stage == "bitcoind_datadir" %}
<p class="warning" style="border: 1px solid #fff;">
<img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/>
<br>Wallets are unavailable if Specter is not connected to Bitcoin Core!<br>
<br>{{ _("Wallets are unavailable if Specter is not connected to Bitcoin Core!") }}<br>
{% if current_user.is_admin %}
<a style="margin: 5px; transform: scale(0.9); font-size: 1em;" class="btn" href="{{ url_for('nodes_endpoint.node_settings', node_alias=specter.node.alias) }}">
<img src="{{ url_for('static', filename='img/gear.svg') }}" style="width: 24px; margin: 0px;" class="svg-white">Configure Node
<img src="{{ url_for('static', filename='img/gear.svg') }}" style="width: 24px; margin: 0px;" class="svg-white">{{ _('Configure Node') }}
</a>
{% endif %}
</p>
@ -207,20 +207,20 @@
</div>
<div class="separator">
<span id="toggle_devices_list" style="cursor: pointer;">Devices &nbsp; &#9660;</span>
<span id="toggle_devices_list" style="cursor: pointer;">{{ _("Devices") }} &nbsp; &#9660;</span>
</div>
{% from 'includes/sidebar/components/sidebar_device_list_item.jinja' import sidebar_device_list_item %}
<div id="devices_list">
{% 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_type'), '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">
<span>Specter Version: <strong>{{ specter.version.current }}</strong></span>
<span>{{ _("Specter Version:") }} <strong>{{ specter.version.current }}</strong></span>
<a style="color: #fff; font-size: 1.1em; margin: 10px; text-align: center;" href="{{ url_for('about') }}">
About Specter<br>
{{ _("About Specter") }}<br>
<img src="{{ url_for('static', filename='img/icon.png') }}" style="width: 35px;"/>
</a>
</div>
@ -262,32 +262,32 @@
}
}
function toggleList(type) {
function toggleList(type, display_text) {
let titleButton = document.getElementById('toggle_' + type + '_list');
let list = document.getElementById(type + '_list');
if (list.style.display === 'none') {
list.style.display = 'block';
titleButton.innerHTML = capitalize(type) + ' &nbsp; &#9660;';
titleButton.innerHTML = capitalize(display_text) + ' &nbsp; &#9660;';
} else {
list.style.display = 'none';
titleButton.innerHTML = capitalize(type) + ' &nbsp; &#9654;';
titleButton.innerHTML = capitalize(display_text) + ' &nbsp; &#9654;';
}
}
document.addEventListener("DOMContentLoaded", function(){
if ('{{ specter.device_manager.devices_names | length }}' == '0') {
document.getElementById('toggle_devices_list').innerHTML = 'Devices';
document.getElementById('toggle_devices_list').innerHTML = '{{ _("Devices") }}';
} else {
document.getElementById('toggle_devices_list').addEventListener('click', (event) => {
toggleList('devices');
toggleList('devices', '{{ _("Devices") }}');
});
}
if ('{{ specter.wallet_manager.wallets_names | length }}' == '0') {
document.getElementById('toggle_wallets_list').innerHTML = 'Wallets';
document.getElementById('toggle_wallets_list').innerHTML = '{{ _("Wallets") }}';
} else {
document.getElementById('toggle_wallets_list').addEventListener('click', (event) => {
toggleList('wallets');
toggleList('wallets', '{{ _("Wallets") }}');
});
}

View file

@ -13,7 +13,7 @@
}
</style>
<div class="tx-data">
<h2>Transaction details</h2><br>
<h2>{{ _("Transaction details") }}</h2><br>
<div class="tx-data-info"></div>
<div class="note"></div>
</div>
@ -36,7 +36,7 @@
this.btcUnit = this.getAttribute('data-btc-unit');
this.price = this.getAttribute('data-price');
this.symbol = this.getAttribute('data-symbol');
this.note.innerText = `Loading transaction details...\n\nTransaction ID: ${this.txid}`;
this.note.innerText = `{{ _("Loading transaction details...\n\nTransaction ID:") }}" ${this.txid}`;
fetchTxData(this);
// Attach the created element to the shadow dom
@ -63,9 +63,9 @@
const jsonResponse = await response.json();
return jsonResponse;
} catch(e) {
self.note.innerText = "Failed to load transaction details...";
self.note.innerText = '{{ _("Failed to load transaction details...") }}';
console.log("Caught error fetching decoded transaction: ", e);
showError('Failed to fetch data. Please refresh the page and retry.');
showError('{{ _("Failed to fetch data. Please refresh the page and retry.") }}');
}
return null;
}
@ -81,17 +81,17 @@
if(tx.is_purged) {
rawtxHTML += `
<div class="warning">
<p><b>This transaction (aka "tx") could not be found in your node's mempool!</b></p>
<p style="text-align:left;">If the bitcoin network is very busy, nodes will start purging the pending txs with the lowest fees. Txs older than two weeks are also purged.</p>
<p style="text-align:left;">A purged tx will never complete. Abandoning this tx will clear it from your wallet, making those funds spendable once again.</p>
<p style="text-align:left;">But first verify that other nodes have also purged your tx! Enter the tx id into a public block explorer (warning: slight privacy leak). If the tx cannot be found, it is safe to abandon this tx.</p>
<p><b>{{ _( "This transaction (aka \"tx\") could not be found in your node's mempool!") }}</b></p>
<p style="text-align:left;">{{ _( "If the bitcoin network is very busy, nodes will start purging the pending txs with the lowest fees. Txs older than two weeks are also purged.") }}</p>
<p style="text-align:left;">{{ _( "A purged tx will never complete. Abandoning this tx will clear it from your wallet, making those funds spendable once again.") }}</p>
<p style="text-align:left;">{{ _( "But first verify that other nodes have also purged your tx! Enter the tx id into a public block explorer (warning: slight privacy leak). If the tx cannot be found, it is safe to abandon this tx.") }}</p>
<br/><br/>
<form method="POST" action="${walletLink}history/">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="abandon_tx"/>
<input type="hidden" name="txid" value="${self.txid}"/>
<button type="submit" name="action" value="abandontx" class="btn danger centered" style="max-width: 160px;">Abandon transaction</button>
<button type="submit" name="action" value="abandontx" class="btn danger centered" style="max-width: 160px;">{{ _("Abandon transaction") }}</button>
</form>
</div>
`;
@ -99,7 +99,7 @@
rawtxHTML += `
<table class="tx-data-table">
<tbody>
<tr><td><span class="optional">Transaction id:</span><span class="mobile-only">TxID:</span></td><td style="word-break: break-all;"><explorer-link data-type="tx" data-value="${self.txid}"></explorer-link></td></tr>
<tr><td><span class="optional">{{ _("Transaction id") }}:</span><span class="mobile-only">TxID:</span></td><td style="word-break: break-all;"><explorer-link data-type="tx" data-value="${self.txid}"></explorer-link></td></tr>
<tr><td>From wallet:</td><td><a href=${walletLink}>${walletName}<a></td></tr>
<tr><td>Size:</td><td>${rawtx.vsize} vbytes <span class="note">(${rawtx.size} bytes)</span></td></tr>
`;
@ -111,20 +111,20 @@
}
}
rawtxHTML += `
<tr><td>Fee:</td><td>${parseInt(-1e8 * fee)} sats</td></tr>
<tr><td>Fee rate:</td><td>${parseFloat((-1e8 * fee / rawtx.vsize).toFixed(2)).toString()} sat/vbyte</td></tr>
<tr><td>{{ _("Fee") }}:</td><td>${parseInt(-1e8 * fee)} sats</td></tr>
<tr><td>{{ _("Fee rate") }}:</td><td>${parseFloat((-1e8 * fee / rawtx.vsize).toFixed(2)).toString()} sat/vbyte</td></tr>
`;
}
if (tx.confirmations && tx.confirmations > 0) {
rawtxHTML += `
<tr><td>Mined at block:</td><td>${tx.blockheight}</td></tr>
<tr><td>Block hash:</td><td style="word-break: break-all;">${tx.blockhash}</td></tr>
<tr><td>Block time:</td><td>${new Date(tx.blocktime * 1000)} <span style="font-size: 0.9em;color: #ccc;">(${tx.blocktime})</span></td></tr>
<tr><td>{{ _("Mined at block") }}:</td><td>${tx.blockheight}</td></tr>
<tr><td>{{ _("Block hash") }}:</td><td style="word-break: break-all;">${tx.blockhash}</td></tr>
<tr><td>{{ _("Block time") }}:</td><td>${new Date(tx.blocktime * 1000)} <span style="font-size: 0.9em;color: #ccc;">(${tx.blocktime})</span></td></tr>
`;
}
rawtxHTML += `
<tr><td>Inputs count:</td><td>${rawtx.vin.length}</td></tr>
<tr><td>Outputs count:</td><td>${rawtx.vout.length}</td></tr>
<tr><td>{{ _("Inputs count") }}:</td><td>${rawtx.vin.length}</td></tr>
<tr><td>{{ _("Outputs count") }}:</td><td>${rawtx.vout.length}</td></tr>
`;
rawtxHTML += `
</tbody>
@ -175,15 +175,15 @@
}
addressAndValue = `<br>
Address: <address-label data-copy-hidden="true" data-address="${address}" data-wallet="${self.wallet}"></address-label><br>
Value: ${value} ${assetlabel} ${price}`;
{{ _("Address:") }} <address-label data-copy-hidden="true" data-address="${address}" data-wallet="${self.wallet}"></address-label><br>
{{ _("Value:") }} ${value} ${assetlabel} ${price}`;
}
if ('coinbase' in rawtx.vin[i]) {
rawtxHTML += `
<p class="tx_info" style="text-align: left; background-color: ${bgColor};">
<b>Input #${i}</b><br><br>
<b>Coinbase transaction</b>
<b>{{ _("Input #") }}${i}</b><br><br>
<b>{{ _("Coinbase transaction") }}</b>
<br>
</p>
`;
@ -191,15 +191,15 @@
}
rawtxHTML += `
<p class="tx_info" style="text-align: left; background-color: ${bgColor};">
<b>Input #${i}</b><br><br>
Transaction id: <explorer-link style="word-break: break-all;" data-type="tx" data-value="${rawtx.vin[i].txid}"></explorer-link><br>
Output # ${rawtx.vin[i].vout}
<b>{{ _("Input #") }}${i}</b><br><br>
{{ _("Transaction id") }}: <explorer-link style="word-break: break-all;" data-type="tx" data-value="${rawtx.vin[i].txid}"></explorer-link><br>
{{ _("Output #") }}" ${rawtx.vin[i].vout}
${addressAndValue}
</p>
`;
}
rawtxHTML += `<h3>Outputs</h3>`;
rawtxHTML += `<h3>{{ _("Outputs") }}</h3>`;
for (let i in rawtx.vout) {
let address = rawtx.vout[i].addresses && rawtx.vout[i].addresses.length == 1 ? rawtx.vout[i].addresses[0] : 'Unknown';
if (address == 'Unknown') {
@ -241,9 +241,9 @@
rawtxHTML += `
<p class="tx_info" style="text-align: left; background-color: ${bgColor};">
<b>Output #${i}</b><br><br>
Address: <address-label data-copy-hidden="true" data-address="${address}" data-wallet="${self.wallet}"></address-label><br>
Value: ${value} ${assetlabel} ${price}
<b>{{ _("Output #${i}") }}</b><br><br>
{{ _("Address:") }} <address-label data-copy-hidden="true" data-address="${address}" data-wallet="${self.wallet}"></address-label><br>
{{ _("Value:") }} ${value} ${assetlabel} ${price}
</p>
`;
}
@ -252,7 +252,7 @@
self.note.innerText = "";
return;
};
self.note.innerText = "Failed to load transaction details...";
self.note.innerText = '{{ _("Failed to load transaction details...") }}';
}
async function fetchAddressIsMine(self, address) {
let url = `{{ url_for('wallets_endpoint.addressinfo', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", self.wallet);
@ -276,7 +276,7 @@
return jsonResponse.isMine;
}
} catch(e) {
showError("Failed to load address data...");
showError('{{ _("Failed to load address data...") }}');
showError(e);
}
return false;

View file

@ -30,8 +30,8 @@
<td class="time"></td>
<td>
<span class="confirmations"></span>
<button class="rbf btn optional hidden" style="width: 130px; float: right;" type="button">Speed up</button>
<button class="rbf-cancel danger btn optional hidden" style="width: 130px; float: right; margin-right: 10px;" type="button">Cancel transaction</button>
<button class="rbf btn optional hidden" style="width: 130px; float: right;" type="button">{{ _("Speed up") }}</button>
<button class="rbf-cancel danger btn optional hidden" style="width: 130px; float: right; margin-right: 10px;" type="button">{{ _("Cancel transaction") }}</button>
</td>
<td class="hidden optional blockhash"></td>
<td><input class="select-tx-value" type="hidden" value=""><img style="vertical-align: middle;" class="select-tx-img" src="{{ url_for('static', filename='img/checkbox-untick.svg') }}" width="25px"></tool-tip></td>
@ -115,7 +115,7 @@
// Set address-label
if (Array.isArray(this.tx.address)) {
this.address.innerText = this.hideSensitiveInfo ? '#########' : `${this.tx.address.length} Recipients`;
this.address.innerText = this.hideSensitiveInfo ? '#########' : `${this.tx.address.length} {{ _("Recipients") }}`;
} else {
this.address.innerHTML = this.hideSensitiveInfo ? '#########' : `<address-label data-address="${this.tx.address}" ${this.tx.label ? `data-label="${this.tx.label}"` : ''} data-wallet="${this.wallet ? this.wallet : this.tx.wallet_alias}"></address-label>`
}
@ -146,7 +146,7 @@
}
if (this.amount == 1e-8) {
this.amountText.innerText = 'Confidential';
this.amountText.innerText = '{{ _("Confidential") }}';
} else {
this.amountText.innerHTML = this.hideSensitiveInfo ? '#########' : `${numberWithCommas(this.amount.toString())}`;
if("assetlabel" in this.tx){
@ -156,12 +156,12 @@
// Set confirmations
if (this.tx.confirmations > 0) {
this.confirmations.innerHTML = this.hideSensitiveInfo ? '########' : `${this.tx.confirmations}<span class="optional"> Confirmations</span>`;
this.confirmations.innerHTML = this.hideSensitiveInfo ? '########' : `${this.tx.confirmations}<span class="optional"> {{ _("Confirmations") }}</span>`;
} else if (this.tx.confirmations == 0) {
this.el.classList.add('unconfirmed');
this.confirmations.innerHTML = this.hideSensitiveInfo ? '########' : `Unconfirmed`;
this.confirmations.innerHTML = this.hideSensitiveInfo ? '########' : `{{ _("Unconfirmed") }}`;
} else {
this.confirmations.innerHTML = this.hideSensitiveInfo ? '########' : `Cancelled (replaced by sender)`;
this.confirmations.innerHTML = this.hideSensitiveInfo ? '########' : `{{ _("Cancelled (replaced by sender)") }}`;
this.category.src = `{{ url_for('static', filename='img') }}/cross.svg`;
this.category.classList.remove('svg-' + this.tx.category);
this.category.classList.add('svg-cancelled');
@ -238,19 +238,19 @@
}
txDataPopup.innerHTML = `
<form action="${url}" method="POST">
<h1>${rbfType == 'cancel' ? 'Cancel' : 'Speed up'} the Transaction</h1>
<p>You can ${rbfType == 'cancel' ? 'cancel' : 'speed up'} the transaction by increasing its fee rate${rbfType == 'cancel' ? ' and sending the funds back to yourself' : ''}:</p>
<h1>${rbfType == 'cancel' ? '{{ _("Cancel") }}' : '{{ _("Speed up") }}'} {{ _("the Transaction") }}</h1>
<p>{{ _("You can") }} ${rbfType == 'cancel' ? '{{ _("cancel") }}' : '{{ _("speed up") }}'} {{ _("the transaction by increasing its fee rate") }}${rbfType == 'cancel' ? ' {{ _("and sending the funds back to yourself") }}' : ''}:</p>
<input type="hidden" name="rbf_tx_id" value='${self.tx.txid}' />
<input type="number" min="${newFee}" value="${newFee}" class="fee_rate" name="rbf_fee_rate" id="rbf_fee_rate" min="1" step="any" autocomplete="off"> sat/vbyte
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<br>
<br>
<button type="submit" name="action" value="${rbfType == 'cancel' ? 'rbf_cancel' : 'rbf'}" class="btn centered ${rbfType == 'cancel' ? 'danger' : ''}">${rbfType == 'cancel' ? 'Cancel transaction' : 'Speed up!'}</button>
<button type="submit" name="action" value="${rbfType == 'cancel' ? 'rbf_cancel' : 'rbf'}" class="btn centered ${rbfType == 'cancel' ? 'danger' : ''}">${rbfType == 'cancel' ? '{{ _("Cancel transaction") }}' : '{{ _("Speed up!") }}'}</button>
<br>
<span class="toggle_advanced_rbf" style="cursor: pointer;">Advanced {% if show_advanced_settings %}&#9660;{% else %}&#9654;{% endif %}</span>
<span class="toggle_advanced_rbf" style="cursor: pointer;">{{ _("Advanced") }} {% if show_advanced_settings %}&#9660;{% else %}&#9654;{% endif %}</span>
<div class="advanced_rbf hidden warning">
<p style="max-width: 400px;">If you would like further customization, you can click here to fully edit the transaction.<br>(advanced, not recommended for new users)</p>
<button type="submit" name="action" value="rbf_edit" class="btn centered">Edit the transaction (advanced)</button>
<p style="max-width: 400px;">{{ _("If you would like further customization, you can click here to fully edit the transaction.") }}<br>{{ _("(advanced, not recommended for new users)") }}</p>
<button type="submit" name="action" value="rbf_edit" class="btn centered">{{ _("Edit the transaction (advanced)") }}</button>
</div>
</form>
`;

View file

@ -96,7 +96,7 @@
<button
type="button"
class="txlist-view-btn btn radio left">
History
{{ _("History") }}
</button>
<button
type="button"
@ -106,7 +106,7 @@
</nav>
<div class="pagination-container">
<a class="pagination-back pagination-arrow pagination-disabled">&#8249;</a>
Page <input class="pagination-idx" type="number" step="1" min="1" value="1"></input><span class="pagination-counter"></span>
{{ _("Page") }} <input class="pagination-idx" type="number" step="1" min="1" value="1"></input><span class="pagination-counter"></span>
<a class="pagination-next pagination-arrow pagination-disabled">&#8250;</a>
<br>
</div>
@ -125,51 +125,51 @@
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="button" class="btn freeze-tx-btn">
<tool-tip title="Freezing UTXOs">
Freezing a UTXO will make Specter disallow spending it.<br><br>
This is useful especially for dealing with dust attack, where you don't want to accidentally spend the dust received.
{{ _("Freezing a UTXO will make Specter disallow spending it.") }}<br><br>
{{ _("This is useful especially for dealing with a dust attack, where you don't want to accidentally spend the dust received.") }}"
</tool-tip>&nbsp;&nbsp;
Un/Freeze UTXOs
</button>
</form>
<form class="compose-tx-form" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="button" class="btn compose-tx-btn">Create new transaction</button>
<button type="button" class="btn compose-tx-btn">{{ _("Create new transaction") }}</button>
</form>
</div>
<div class="export-container hidden">
<h1>Export transactions to CSV</h1>
<h1>{{ _("Export transactions to CSV") }}</h1>
<div class="{% if not specter.price_check or not (specter.alt_rate and specter.alt_symbol) %}hidden{% endif %}">
Fetch historical price data:
{{ _("Fetch historical price data") }}:
<label class="switch">
<input type="checkbox" class="export-prices">
<span class="slider"></span>
</label><br><br>
<div class="note">
If enabled, specter will try to fetch historical prices for each transaction date from the configured price provider.
<br><b>This has privacy implications!</b>
{{ _("If enabled, specter will try to fetch historical prices for each transaction date from the configured price provider.") }}
<br><b>{{ _("This has privacy implications!") }}</b>
</div><br>
</div>
<a class="export btn" download>Export</a>
<a class="export btn" download>{{ _("Export") }}</a>
</div>
<table class="tx-table">
<thead>
<tr>
<th value="category" class="category-header"><div class="category-arrow"></div></th>
<th value="txid" class="optional txid-header">TxID<div class="txid-arrow"></div></th>
<th value="label" class="optional label-header">Label<div class="label-arrow"></div></th>
<th value="amount" class="amount-header">Amount<div class="amount-arrow"></div></th>
<th value="time" class="time-header">Time<div class="time-arrow up-arrow"></div></th>
<th value="confirmations" class="confirmations-header">Confirmations<div class="confirmations-arrow"></div></th>
<th value="label" class="optional label-header">{{ _("Label") }}<div class="label-arrow"></div></th>
<th value="amount" class="amount-header">{{ _("Amount") }}<div class="amount-arrow"></div></th>
<th value="time" class="time-header">{{ _("Time") }}<div class="time-arrow up-arrow"></div></th>
<th value="confirmations" class="confirmations-header">{{ _("Confirmations") }}<div class="confirmations-arrow"></div></th>
<th value="blockhash" class="optional hidden blockhash-header">
{% include "includes/merkletooltip.html" %}
Block Hash
{{ _("Block Hash") }}
</th>
<th style="width: 50px; padding: 10px;"><button type="button" class="export-btn btn" style="width: 50px;">Export</button></th>
<th style="width: 50px; padding: 10px;"><button type="button" class="export-btn btn" style="width: 50px;">{{ _("Export") }}</button></th>
</tr>
</thead>
<tr class="empty">
<td></td>
<td>Fetching transactions...</td>
<td>{{ _("Fetching transactions...") }}</td>
<td class="optional"></td>
<td class="optional"></td>
<td></td>
@ -481,11 +481,11 @@
self.callId++;
// Clean up from existing transaction list
self.summaryRow.classList.add('hidden');
self.emptyRow.children[1].innerText = "Fetching transactions...";
self.emptyRow.children[1].innerText = '{{ _("Fetching transactions...") }}';
self.emptyRow.classList.remove('hidden');
self.tbody.querySelectorAll('tx-row').forEach((row) => {row.remove()});
if (!self.listType) {
self.emptyRow.children[1].innerText = "Failed to fetch transactions...";
self.emptyRow.children[1].innerText = '{{ _("Failed to fetch transactions...") }}';
return
}
@ -518,7 +518,7 @@
break
default:
self.export.href = ``
self.emptyRow.children[1].innerText = "Failed to fetch transactions...";
self.emptyRow.children[1].innerText = '{{ _("Failed to fetch transactions...") }} ';
return
}
@ -592,7 +592,7 @@
// Show empty row if no transaction found
if (self.pageCount == 0 || txlist.length == 0) {
self.emptyRow.classList.remove('hidden');
self.emptyRow.children[1].innerText = "No transactions found...";
self.emptyRow.children[1].innerText = '{{ _("No transactions found...") }}';
self.pageCount = 1;
} else {
self.emptyRow.classList.add('hidden');
@ -654,10 +654,10 @@
}
return
};
showError('Failed to fetch transactions list.')
showError('{{ _("Failed to fetch transactions list.") }}')
} catch(e) {
console.log("Caught error: ", e);
showError(`Failed to fetch transactions list: ${e}`);
showError(`{{ _("Failed to fetch transactions list.") }}: ${e}`);
}
}
@ -682,7 +682,7 @@
</tx-data>`;
showPageOverlay('tx-popup');
} else {
copyText(txid, `Copied transaction: ${ txid }`);
copyText(txid, `{{ _("Copied transaction") }}: ${ txid }`);
}
}

View file

@ -5,18 +5,18 @@
{% block main %}
<div class="card">
<h1>Login to Specter</h1>
<h1>{{ _("Login to Specter") }}</h1>
<form action="{{ url_for('auth_endpoint.login') }}" method="POST" role="form">
{% if specter.config['auth']['method'] == 'usernamepassword' %}
<input id="username" class="form-control" placeholder="Username" name="username" type="text" value=""><br><br>
<input id="username" class="form-control" placeholder="{{ _('Username') }}" name="username" type="text" value=""><br><br>
{% endif %}
<input id="password" class="form-control" placeholder="Password" name="password" type="password" value="">
<input id="password" class="form-control" placeholder="Password" name="{{ _('password') }}" type="password" value="">
<input type="hidden" name="next" value="{{ data.next }}">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<br>
<br>
<div class="row">
<button type="submit" class="btn">Login</button>
<button type="submit" class="btn">{{ _("Login") }}</button>
</div>
</form>
</div>

View file

@ -3,19 +3,19 @@
<form action="?" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<h1 id="title" class="settings-title">Settings</h1>
<h1 id="title" class="settings-title">{{ _("Settings") }}</h1>
{% from 'settings/components/settings_menu.jinja' import settings_menu %}
{% from 'settings/components/settings_menu_item.jinja' import settings_menu_item %}
{{ settings_menu('bitcoin_core', current_user) }}
</form>
<br><br><br>
<a href="{{ url_for('nodes_endpoint.node_settings', node_alias=node_alias) }}" class="btn centered">Return</a>
<a href="{{ url_for('nodes_endpoint.node_settings', node_alias=node_alias) }}" class="btn centered">{{ _("Return") }}</a>
<br><br>
<div class="card" style="width:90%; max-width: 1000px;">
Bitcoin Logs:
<pre><code>{{ loglines }}</code></pre>
<br>
<div>If this is an issue, please let us know! Create an <a href="https://github.com/cryptoadvance/specter-desktop/issues" target="_blank" style="color: #fff;">issue</a> or ping us on <a href="https://t.me/spectersupport" target="_blank" style="color: #fff;">Telegram</a> to fix it.</div>
<div>{{ _('If this is an issue, please let us know! Create an <a href="https://github.com/cryptoadvance/specter-desktop/issues" target="_blank" style="color: #fff;">issue</a> or ping us on <a href="https://t.me/spectersupport" target="_blank" style="color: #fff;">Telegram</a> to fix it.') }}</div>
</div>
{% endblock %}

View file

@ -6,43 +6,43 @@
<form action="?" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="card" style="margin: 20px auto 120px; text-align: center;">
<h2>Built in Bitcoin Node Status: {% if node.bitcoind.status %}{{node.bitcoind.status}}{% else %}Down{% endif %}</h2><br>
<span class="note" style="font-size: 1.3em;">Bitcoin Core Version: {{node.version}}</span>
<h2>{{ _("Built-in Bitcoin Node Status:") }} {% if node.bitcoind.status %}{{node.bitcoind.status}}{% else %}Down{% endif %}</h2><br>
<span class="note" style="font-size: 1.3em;">{{ _("Bitcoin Core Version:") }} {{node.version}}</span>
{% if node.version != latest_bitcoind %}
<br><br>
<div class="warning" style="padding: 30px;">
<h1 style="text-align: center; color: #000">New Bitcoin version available!</h1>
<button type="submit" name="action" value="upgrade_bitcoind" class="btn action centered">Upgrade Bitcoin Core version to: v{{ latest_bitcoind }}</button>
<h1 style="text-align: center; color: #000">{{ _("New Bitcoin version available!") }}</h1>
<button type="submit" name="action" value="upgrade_bitcoind" class="btn action centered">{{ _("Upgrade Bitcoin Core version to:") }} v{{ latest_bitcoind }}</button>
</div>
{% endif %}
<br><br>
{% if node.bitcoind.status != "Down" %}
<button type="submit" class="btn centered" name="action" value="stopbitcoind">Stop Bitcoin Core</button>
<button type="submit" class="btn centered" name="action" value="stopbitcoind">{{ _("Stop Bitcoin Core") }}</button>
{% else %}
<button type="submit" class="btn centered" name="action" value="startbitcoind">Start Bitcoin Core</button>
<button type="submit" class="btn centered" name="action" value="startbitcoind">{{ _("Start Bitcoin Core") }}</button>
<p class="warning">
If this is your first time starting the node, and you haven't used the QuickSync, your node will start with Initial Block Downloading (IBD) to sync with the network.<br>
This process of IBD syncing may take several days. If you'd like the QuickSync option, please click on it before starting your node, or if it's running, stop it and the click the QuickSync.<br>
<b>(Warning: QuickSync will override any existing data of your Bitcoin Node!)</b>
{{ _("If this is your first time starting the node, and you haven't used the QuickSync, your node will start with Initial Block Downloading (IBD) to sync with the network.") }}<br>
{{ _("This process of IBD syncing may take several days. If you'd like the QuickSync option, please click on it before starting your node, or if it's running, stop it and the click the QuickSync.") }}<br>
<b>{{ _("(Warning: QuickSync will override any existing data of your Bitcoin Node!)") }}</b>
</p>
{% endif %}
<br><br>
<br><br>
<div class="card">
<h1>Debug</h1>
<a class="btn centered" href="{{url_for('nodes_endpoint.internal_node_logs', node_alias=node_alias)}}">See Bitcoind Logs</a>
<h1>{{ _("Debug") }}</h1>
<a class="btn centered" href="{{url_for('nodes_endpoint.internal_node_logs', node_alias=node_alias)}}">{{ _("See Bitcoind Logs") }}</a>
<br><br>
<h1>Danger Zone</h1>
<h1>{{ _("Danger Zone") }}</h1>
{% if specter.node_manager.nodes | length > 1 and node_alias %}
<div class="row">
<button type="submit" name="action" value="forget" class="btn danger centered">Forget node</button>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<button type="submit" name="action" value="forget" class="btn danger centered">{{ _("Forget node") }}</button>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
{% endif %}
<button type="submit" name="action" value="uninstall_bitcoind" class="btn danger centered">Unistall Bitcoin Core</button>
<button type="submit" name="action" value="uninstall_bitcoind" class="btn danger centered">{{ _("Unistall Bitcoin Core") }}</button>
{% if specter.node_manager.nodes | length > 1 and node_alias %}
</div>
{% endif %}
<br>
<label><input type="checkbox" class="inline" name="remove_datadir"> Delete data folder?</label>
<label><input type="checkbox" class="inline" name="remove_datadir"> {{ _("Delete data folder?") }}</label>
</div>
</div>
</form>

View file

@ -9,14 +9,14 @@
<form action="?" method="POST">
{% else %}
<form action="?" method="POST">
<h1 id="title">Configure new node</h1>
<label style="text-align: center; display: block; margin: auto;font-size: 1.2em; margin-bottom: 7px;">Name your new node:</label>
<input type="text" id="name" name="name" value="{{ node.name }}" placeholder="Name your new node" style="width: auto; display: block; margin: auto; width: 70%;">
<h1 id="title">{{ _("Configure new node") }}</h1>
<label style="text-align: center; display: block; margin: auto;font-size: 1.2em; margin-bottom: 7px;">{{ _("Name your new node:") }}</label>
<input type="text" id="name" name="name" value="{{ node.name }}" placeholder='{{ _("Name your new node") }}' style="width: auto; display: block; margin: auto; width: 70%;">
{% endif %}
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="card" style="margin: 20px auto 20px;">
<h2>Bitcoin JSON-RPC configuration</h2>
<h2>{{ _("Bitcoin JSON-RPC configuration") }}</h2>
<br>
<label style="height: 25px; vertical-align: sub;">Auto-detect:&nbsp;</label>
<label class="switch">
@ -28,22 +28,21 @@
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Setting Bitcoin Core Data Directory<br></span><br>
When auto-detect is on, Specter will check for Environment-Variables (BTC_RPC_USER, BTC_RPC_PASSWORD, BTC_RPC_HOST, BTC_RPC_PORT) to configure the connection or
attempt to automatically locate your Bitcoin data directory and load your node configurations from it.<br><br>
However, if your Bitcoin Core data directory is not located at the default location, you will need to enter its path here so Specter will be able to locate it.<br><br>
If you are connecting to a specific remote node, you can disable the auto-detect feature and enter the node's configurations manually below.
<span class="info__title">{{ _("Setting Bitcoin Core Data Directory") }}<br></span><br>
{{ _("When auto-detect is on, Specter will check for Environment-Variables (BTC_RPC_USER, BTC_RPC_PASSWORD, BTC_RPC_HOST, BTC_RPC_PORT) to configure the connection or attempt to automatically locate your Bitcoin data directory and load your node configurations from it.") }}<br><br>
{{ _("However, if your Bitcoin Core data directory is not located at the default location, you will need to enter its path here so Specter will be able to locate it.") }}<br><br>
{{ _("If you are connecting to a specific remote node, you can disable the auto-detect feature and enter the node's configurations manually below.") }}
</span>
</p>
</div>
<br><br>
Bitcoin Core data directory path:<br><input type="text" id="datadir" name="datadir" type="text" value="{{ node.datadir }}">
{{ _("Bitcoin Core data directory path:") }}<br><input type="text" id="datadir" name="datadir" type="text" value="{{ node.datadir }}">
</span>
<br><br>
<div id="rpc_settings">
<div id="lock" class="center">
<p style="text-align: center; margin: auto; ">&#128274;</p>
<span class="note centered"> (disable auto-detect to configure manually)</span>
<span class="note centered"> {{ _("(disable auto-detect to configure manually)") }}</span>
</div>
Username:<br><input type="text" id="username" name="username" value="{{ node.user }}">
<br><br>
@ -54,21 +53,21 @@
<qr-scanner id="scan_rpc" style="margin: 10px;">
<a slot="button" href="#" class="btn" style="padding: 7px 15px 5px 10px;">
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 30px; margin: 0px;" class="svg-white">
Connect with QR
{{ _("Connect with QR") }}
</a>
</qr-scanner>
</div>
Port:<br><input type="text" id="port" name="port" type="text" value="{{ node.port }}">
<div class="note">
Default ports: <b>8332</b> for mainnet, <b>18332</b> for Testnet, <b>18443</b> for Regtest, <b>38332</b> for Signet, <b>7041</b> for Liquid
{{ _("Default ports: <b>8332</b> for mainnet, <b>18332</b> for Testnet, <b>18443</b> for Regtest, <b>38332</b> for Signet, <b>7041</b> for Liquid") }}
</div>
<br><br>
<div class="row">
<button type="submit" class="btn" name="action" value="test">Test</button>&nbsp;
<button type="submit" class="btn" name="action" value="save">Save</button>
<button type="submit" class="btn" name="action" value="test">{{ _("Test") }}</button>&nbsp;
<button type="submit" class="btn" name="action" value="save">{{ _("Save") }}</button>
</div>
{% if test %}
<br><div class="log"><b>Test results:</b><br><br>
<br><div class="log"><b>{{ _("Test results:") }}</b><br><br>
<div style="display: grid;grid-template-columns: auto auto auto; line-height: 2.2;">
{% macro tick_or_cross(my_boolean) %}
<button style="background: #fff; border: none;" disabled>
@ -80,15 +79,12 @@
</button>
{% endmacro %}
{% if 'connectable' in test['tests'] %}
<div>Connectable</div>
<div>{{ _("Connectable") }}</div>
<div>{{ tick_or_cross(test['tests']['connectable']) }} </div>
{% if not test['tests']['connectable'] %}
<tool-tip title="Your Node can't be reached" style="float: right;">
There are a lot of potential issues preventing the connection.
Please doublecheck the Host and the Port. Make sure you can reach
the host and make sure that Bitcoin-Core is listening at the port
you have specified.
For more hints, please look at this <a style="color:grey" href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/connect-your-node.md" target="_blank">article</a>.
<tool-tip title="{{ _("Your Node can't be reached") }}" style="float: right;">
{{ _("There are a lot of potential issues preventing the connection. Please double check the Host and the Port. Make sure you can reach the host and make sure that Bitcoin-Core is listening at the port you have specified.") }}
{{ _('For more hints, please look at this <a style="color:grey" href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/connect-your-node.md" target="_blank">article</a>.') }}
</tool-tip>
{% else %}
<div></div>
@ -99,10 +95,10 @@
<div>Credentials</div>
<div> {{ tick_or_cross(test['tests']['credentials']) }} </div>
{% if not test['tests']['credentials'] %}
<tool-tip title="Your Credentials don't work" style="float: right;">
Please doublecheck the user and the Password. Look at the bitcoin.conf
for the correct values in that field.
For more hints, please look at this <a style="color:grey" href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/connect-your-node.md" target="_blank">article</a>.
<tool-tip title="{{ _("Your Credentials don't work") }}" style="float: right;">
{{ _("Please doublecheck the user and the Password. Look at the bitcoin.conf
for the correct values in that field.") }}
{{ _('For more hints, please look at this <a style="color:grey" href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/connect-your-node.md" target="_blank">article</a>.') }}
</tool-tip>
{% else %}
<div></div>
@ -110,13 +106,11 @@
{% endif %}
{% if 'recent_version' in test['tests'] %}
<div>Version recent enough</div>
<div>{{ _("Version recent enough") }}</div>
<div> {{ tick_or_cross(test['tests']['recent_version']) }} </div>
{% if not test['tests']['recent_version'] %}
<tool-tip title="Your Core Node might be too old" style="float: right; margin-bottom: 5px;">
Specter is working well up from Bitcoin Core version 0.17.
The version of your Node is too low, unfortunatley.
Please upgrade!
<tool-tip title='{{ _("Your Core Node might be too old") }}' style="float: right; margin-bottom: 5px;">
{{ _("Specter is working well up from Bitcoin Core version 0.17. The version of your Node is too low, unfortunatley. Please upgrade!") }}
</tool-tip>
{% else %}
<div></div>
@ -127,9 +121,8 @@
<div>Wallets enabled</div>
<div> {{ tick_or_cross(test['tests']['wallets']) }} </div>
{% if not test['tests']['wallets'] %}
<tool-tip title="Wallet Support in your Core-Node" style="float: right; margin-bottom: 5px;">
The RPC-Interface of your Core-Node is available, but the wallet-api is not.
Please make sure to have 'disablewallet=0' in your bitcoin.conf.
<tool-tip title='{{ _("Wallet Support in your Core-Node") }}' style="float: right; margin-bottom: 5px;">
{{ _("The RPC-Interface of your Core-Node is available, but the wallet-api is not. Please make sure to have 'disablewallet=0' in your bitcoin.conf.") }}
</tool-tip>
{% else %}
<div></div>
@ -140,13 +133,13 @@
<br>
<code>
<pre>Process finished with code <b>{{ test.code }}</b>{% if test.code == 0 %} Output: {{ test.out }}{% else %}Error message: {{ test.err }}{% endif %}</pre>
<pre>{{ _("Process finished with code ") }}<b>{{ test.code }}</b>{% if test.code == 0 %} {{ _("Output:") }} {{ test.out }}{% else %}{{ _("Error message:") }} {{ test.err }}{% endif %}</pre>
</code>
</div>
{% endif %}
</div>
{% if specter.node_manager.nodes | length > 1 and node_alias %}
<button style="margin-bottom: 100px;" type="submit" name="action" value="forget" class="btn danger centered">Forget node</button>
<button style="margin-bottom: 100px;" type="submit" name="action" value="forget" class="btn danger centered">{{ _("Forget node") }}</button>
{% endif %}
</form>
{% endblock %}
@ -211,7 +204,7 @@
document.getElementById('host').value = host;
document.getElementById('port').value = port;
} catch {
showError('Failed to read connection data from the QR', 3000)
showError('{{ _("Failed to read connection data from the QR") }}', 3000)
}
});
}

View file

@ -5,18 +5,18 @@
{% block main %}
<div class="card">
<h1>Register to the Specter Node</h1>
<h1>{{ _("Register to the Specter Node") }}</h1>
<form action="{{ url_for('auth_endpoint.register') }}" method="POST" role="form">
{% if specter.config['auth']['method'] == 'usernamepassword' %}
<input class="form-control" placeholder="Username" name="username" type="text" value=""><br><br>
<input class="form-control" placeholder='{{ _("Username") }}' name="username" type="text" value=""><br><br>
{% endif %}
<input class="form-control" placeholder="Password" name="password" type="password" value="">
<input class="form-control" placeholder='{{ _("Password") }}' name="password" type="password" value="">
<input type="hidden" name="otp" value="" id="otp">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<br>
<br>
<div class="row">
<button type="submit" class="btn">Register</button>
<button type="submit" class="btn">{{ _("Register") }}</button>
</div>
</form>
</div>

View file

@ -7,30 +7,30 @@
{{ settings_menu('auth', current_user) }}
<div class="card" style="margin: 20px auto;">
{% if method == "none" or current_user.is_admin %}
Authentication:<br>
{{ _("Authentication") }}:<br>
<select name="method" onchange="toggleUsernamePassword(this)">
<option value="none" {% if method=="none" %} selected="selected"{% endif %}>None</option>
<option value="passwordonly" {% if method=="passwordonly" %} selected="selected"{% endif %}>Password Protection</option>
<option value="usernamepassword" {% if method=="usernamepassword" %} selected="selected"{% endif %}>Multiple Users</option>
<option value="rpcpasswordaspin" {% if method=="rpcpasswordaspin" %} selected="selected"{% else %}{% if specter.rpc is none or specter.node.autodetect %}disabled{% endif %}{% endif %}>RPC password as Pin</option>
<option value="none" {% if method=="none" %} selected="selected"{% endif %}>{{ _("None") }}</option>
<option value="passwordonly" {% if method=="passwordonly" %} selected="selected"{% endif %}>{{ _("Password Protection") }}</option>
<option value="usernamepassword" {% if method=="usernamepassword" %} selected="selected"{% endif %}>{{ _("Multiple Users") }}</option>
<option value="rpcpasswordaspin" {% if method=="rpcpasswordaspin" %} selected="selected"{% else %}{% if specter.rpc is none or specter.node.autodetect %}disabled{% endif %}{% endif %}>{{ _("RPC password as Pin") }}</option>
</select>
<br><br>
{% endif %}
<div id="ratelimit" class="{% if method == 'none' or not current_user.is_admin %}hidden{% endif %}">
Rate Limiting (seconds between login/register attempts):<br><input id="rate_limit" type="number" name="rate_limit" min="0" step="1" value="{{ rate_limit }}"><br><br>
{{ _("Rate Limiting (seconds between login/register attempts)") }}:<br><input id="rate_limit" type="number" name="rate_limit" min="0" step="1" value="{{ rate_limit }}"><br><br>
</div>
<div id="passwordonly" class="{% if method != 'passwordonly' %}hidden{% endif %}">
Specter Password:<br><input id="specter_password_only" type="password" name="specter_password_only" placeholder="Set new password">
<br><span class="note">(default: admin)</span><br>
{{ _("Specter Password") }}:<br><input id="specter_password_only" type="password" name="specter_password_only" placeholder="Set new password">
<br><span class="note">({{ _("default") }}: admin)</span><br>
<br>
</div>
<div id="usernamepassword" class="{% if method != 'usernamepassword' %}hidden{% endif %}">
{% if current_user.is_admin %}
Registration Link Timeout (hours):<br><input id="registration_link_timeout" type="number" name="registration_link_timeout" min="0" step="1" value="{{ registration_link_timeout }}"><br><br>
<span class="note">Default is username: admin, password: admin.</span><br>
{{ _("Registration Link Timeout (hours)") }}:<br><input id="registration_link_timeout" type="number" name="registration_link_timeout" min="0" step="1" value="{{ registration_link_timeout }}"><br><br>
<span class="note">{{ _("Default username is 'admin', password 'admin'.") }}</span><br>
{% endif %}
Specter Username:<br><input id="specter_username" type="text" name="specter_username" type="text" value="{{ current_user.username }}"><br><br>
Specter Password:<br><input id="specter_password" type="password" name="specter_password" type="text" placeholder="Set new password"><br>
{{ _("Specter Username") }}:<br><input id="specter_username" type="text" name="specter_username" type="text" value="{{ current_user.username }}"><br><br>
{{ _("Specter Password") }}:<br><input id="specter_password" type="password" name="specter_password" type="text" placeholder="Set new password"><br>
<br><br>
{% if users %}
<h2>Users:</h2>
@ -38,9 +38,9 @@
<form action="?" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="row break-row-mobile" style="margin: 15px; border: 1px solid var(--cmap-border); border-radius: 4px; padding: 10px;">
<span style="line-height: 35px; vertical-align: middle;width: 100%;"><b>Username:</b> {{ user.username }}</span>
<span style="line-height: 35px; vertical-align: middle;width: 100%;"><b>{{ _("Username") }}:</b> {{ user.username }}</span>
<br class="mobile-only">
<button type="submit" name="action" value="deleteuser" style="float: right; max-width: 250px;" class="btn danger">Delete User</button>
<button type="submit" name="action" value="deleteuser" style="float: right; max-width: 250px;" class="btn danger">{{ _("Delete User") }}</button>
<input type="hidden" name="deleteuser" value="{{ user.id }}">
<br class="mobile-only">
<br class="mobile-only">
@ -50,12 +50,12 @@
{% endif %}
</div>
<div class="row">
<button type="submit" class="btn" name="action" value="save">Save</button>
<button type="submit" class="btn" name="action" value="save">{{ _("Save") }}</button>
</div>
{% if method == "usernamepassword" and current_user.is_admin %}
<br>
<div id="generateregistrationlink" class="row{% if method != 'usernamepassword' %} hidden{% endif %}">
<button type="submit" class="btn" name="action" value="adduser">Generate New Registration Link</button>
<button type="submit" class="btn" name="action" value="adduser">{{ _("Generate New Registration Link") }}</button>
</div>
{% endif %}
</div>

View file

@ -8,9 +8,9 @@
#}
{% macro settings_menu(active_menuitem, current_user) -%}
<nav class="row collapse-on-mobile">
{{ settings_menu_item('general', 'General', active_menuitem, isLeft=true) }}
{{ settings_menu_item('auth', 'Authentication', active_menuitem, isRight=false) }}
{{ settings_menu_item('hwi', 'USB Devices', active_menuitem, isRight=(not current_user.is_admin)) }}
{{ settings_menu_item('general', _('General'), active_menuitem, isLeft=true) }}
{{ settings_menu_item('auth', _('Authentication'), active_menuitem, isRight=false) }}
{{ settings_menu_item('hwi', _('USB Devices'), active_menuitem, isRight=(not current_user.is_admin)) }}
{% if current_user.is_admin %}
{{ settings_menu_item('tor', 'Tor', active_menuitem, isRight=true) }}
{% endif %}

View file

@ -6,75 +6,78 @@
{% from 'settings/components/settings_menu.jinja' import settings_menu %}
{{ settings_menu('general', current_user) }}
<div class="card" style="margin: 20px auto;">
<h1> Backup and Restore </h1>
<h1>{{ _("Language") }}</h1>
{% include "includes/language/language_select.jinja" %}<br/>
<br><br>
<h1>{{ _("Backup and Restore") }}</h1>
<div class="tool-tip" style="float: right; margin-bottom: 5px;">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Backing up Your Specter Data<br><br></span>
It is recommended that you keep your backup files private, as sharing them will result in privacy leaks.<br><br>
Backups are recommended to ensure that access to funds remain, even in the event that a multisig wallet is lost.
<span class="info__title">{{ _("Backing up Your Specter Data") }}<br><br></span>
{{ _("It is recommended that you keep your backup files private, as sharing them will result in privacy leaks.") }}<br><br>
{{ _("Backups are recommended to ensure that access to funds remain, even in the event that a multisig wallet is lost.") }}
</span>
</p>
</div>
Specter Data Backup:
{{ _("Specter Data Backup") }}:
<br><br>
<div class="row">
<a href="{{ url_for('settings_endpoint.backup_file') }}" class="btn" style="width: 100%; margin-top: -5px;">Download Specter backup files</a>
<a href="{{ url_for('settings_endpoint.backup_file') }}" class="btn" style="width: 100%; margin-top: -5px;">{{ _("Download Specter backup files") }}</a>
</div>
<br>
<div class="tool-tip" style="float: right; margin-bottom: 5px;">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Restoring Specter from backup<br><br></span>
Here you can restore your wallets and devices from an existing Specter backup.<br>
Please make sure to unzip the backup file first, then select the extracted folder for upload.<br>
Note: Loading devices/ wallets with names identical to existing ones may overwrite the existing ones.
<span class="info__title">{{ _("Restoring Specter from backup") }}<br><br></span>
{{ _("Here you can restore your wallets and devices from an existing Specter backup.") }}<br>
{{ _("Please make sure to unzip the backup file first, then select the extracted folder for upload.") }}<br>
{{ _("Note: Loading devices/ wallets with names identical to existing ones may overwrite the existing ones.") }}
</span>
</p>
</div>
<span style="margin: auto 5px auto 0px;">Load Specter backup:</span><br><br>
<span style="margin: auto 5px auto 0px;">{{ _("Load Specter backup") }}:</span><br><br>
<div class="row">
<input type="file" id="file" class="inputfile" webkitdirectory mozdirectory/>
<input type="hidden" id="restorewallets" name="restorewallets"/>
<input type="hidden" id="restoredevices" name="restoredevices"/>
<label for="file" class="btn" style="margin-top: -5px;">
Choose backup folder
{{ _("Choose backup folder") }}
</label>
</div>
<span id="filesloaded"></span>
<div class="row" style="margin-top: 5px;">
<button onsubmit="showPacman()" type="submit" class="btn hidden" name="action" value="restore" id="restore" style="margin-bottom: 5px;">Load Specter backup</button>
<button onsubmit="showPacman()" type="submit" class="btn hidden" name="action" value="restore" id="restore" style="margin-bottom: 5px;">{{ _("Load Specter backup") }}</button>
</div><br><br>
<h1> Miscellaneous </h1>
Bitcoin unit to use (BTC/sats):
<h1>{{ _("Miscellaneous") }} </h1>
{{ _("Bitcoin unit to use") }} (BTC/sats):
<select name="unit">
<option value="btc" {% if unit!="sat" %} selected="selected"{% endif %}>BTC</option>
<option value="sat" {% if unit=="sat" %} selected="selected"{% endif %}>sats</option>
</select>
<br><br>
Block explorer URL ({{ specter.chain }}):<br>
{{ _("Block explorer URL") }} ({{ specter.chain }}):<br>
{% include "components/explorer_select.jinja" %}
<div class="warning">
<img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>
Specter does not use the block explorer to get any data whatsoever.
This setting is only to allow opening transactions and addresses in a block explorer directly from Specter.
All data Specter uses comes directly from your own connected full node.
{{ _("Specter does not use the block explorer to get any data whatsoever. This setting is only to allow opening transactions and addresses in a block explorer directly from Specter. All data Specter uses comes directly from your own connected full node.") }}
</div>
<br>
Fees estimation source:<br>
{{ _("Fees estimation source") }}:<br>
<label><input type="radio" class="inline" style="margin: 0 10px 0 20px;" name="fee_estimator" value="mempool" onclick="updateFeeEstimator()" {% if specter.fee_estimator=="mempool" %}checked{% endif %}>Mempool.space</label><br>
<label><input type="radio" class="inline" style="margin: 0 10px 0 20px" name="fee_estimator" value="bitcoin_core" onclick="updateFeeEstimator()" {% if specter.fee_estimator=="bitcoin_core" %}checked{% endif %}>Bitcoin Core</label><br>
<label><input type="radio" class="inline" style="margin: 0 10px 0 20px;" name="fee_estimator" value="custom" onclick="updateFeeEstimator()" id="fee_estimator_custom_option" {% if specter.fee_estimator=="custom" %}checked{% endif %}>Self hosted mempool.space</label>
<label><input type="radio" class="inline" style="margin: 0 10px 0 20px;" name="fee_estimator" value="custom" onclick="updateFeeEstimator()" id="fee_estimator_custom_option" {% if specter.fee_estimator=="custom" %}checked{% endif %}>{{ _("Self hosted mempool.space") }}</label>
<br>
<input type="text" name="fee_estimator_custom_url" id="fee_estimator_custom_url" value="{{ fee_estimator_custom_url }}" class="hidden" placeholder="http://yourmempool.space/" style="margin-top: 15px;"/>
<br><br>
{% if current_user.is_admin %}
Log Level:
{{ _("Log Level") }}:
<tool-tip title="Logging" style="float: right;">
Specter-Desktop created a Logfile in <code>{{ config.SPECTER_LOGFILE}}</code>. The Debug-Level will
create a lot of data, so make sure to avoid that if you don't have a reason.
{{ _("Specter-Desktop created a Logfile in") }} <code>{{ config.SPECTER_LOGFILE}}</code>. {{ _("The Debug-Level will create a lot of data, so make sure to avoid that if you don't have a reason.") }}
</tool-tip>
<br>
<select name="loglevel">
@ -85,30 +88,31 @@
<br><br>
{% endif %}
Validate Merkle Proofs:
{{ _("Validate Merkle Proofs") }}:
<div class="note">
Cannot enable when using a pruned bitcoin node
{{ _("Cannot enable when using a pruned bitcoin node") }}
</div>
<div class="row">
<label class="switch">
<input type="checkbox" id="validatemerkleproof" name="validatemerkleproof" {% if validate_merkle_proofs %}checked{% endif %}>
<span class="slider"></span>
</label>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<span class="note explorer-link" style="text-decoration: underline;" onclick="toggleHidden('merkle-info-details');">What's this?</span>
<span class="note explorer-link" style="text-decoration: underline;" onclick="toggleHidden('merkle-info-details');">{{ _("What's this?") }}</span>
</div>
<div class="warning hidden" id="merkle-info-details">
<img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>
Specter-Desktop validates <a href="https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki" target="_blank">BIP 37</a> Merkle Proofs from your full node <strong>guaranteeing</strong> that transactions displayed as confirmed have been included in the specified block hash.<br><br>
However, a compromised full node could return a fake block that was never mined into Bitcoin's blockchain!
If you find the displayed block hash in other locations (other nodes, block explorers, etc) <strong>and you trust specter-desktop isn't lying to you</strong>, then you can be sure this transaction exists in their blockchain as well.
{{ _('Specter-Desktop validates <a href="https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki" target="_blank">BIP 37</a> Merkle Proofs from your full node <strong>guaranteeing</strong> that displayed as confirmed have been included in the specified block hash.') }}<br><br>
{{ _("However, a compromised full node could return a fake block that was never mined into Bitcoin's blockchain! If you find the displayed block hash in other locations (other nodes, block explorers, etc) <strong>and you trust specter-desktop isn't lying to you</strong>, then you can be sure this transaction exists in their blockchain as well.") }}
</div>
<br>
<div class="row">
<button type="submit" class="btn" name="action" value="save">Save</button>
<button type="submit" class="btn" name="action" value="save">{{ _("Save") }}</button>
</div>
</div>
</form>
{% endblock %}
{% block scripts %}
<script type="text/javascript">
function toggleHidden(elementId) {
@ -164,5 +168,8 @@
});
}
});
{% include "includes/language/language_js.jinja" %}
</script>
{% endblock %}

View file

@ -7,27 +7,27 @@
}
</style>
{% include "includes/hwi/hwi.jinja" %}
<h1 id="title" class="settings-title">Settings</h1>
<h1 id="title" class="settings-title">{{ _("Settings") }}</h1>
{% from 'settings/components/settings_menu.jinja' import settings_menu %}
{{ settings_menu('hwi', current_user) }}
<br>
<h1>Hardware Devices Bridge</h1>
<p class="note center">Allows Specter to communicate with your hardware devices over USB.</p>
<h1>{{ _("Hardware Devices Bridge") }}</h1>
<p class="note center">{{ _("Allows Specter to communicate with your hardware devices over USB.") }}</p>
<br>
<label class="card radio-btn" style="padding: 20px 10px 5px;">
<div class="row">
<input type="radio" name="hwibridge" value="local" style="width: 20px; min-width: 20px; margin-top: 20px;" onchange="switchHWIBridgeConfView(this.value)" {% if specter.hwi_bridge_url == '/hwi/api/'%} checked {% endif %}/>
<h2 style="text-align: left; margin-left: 10px;">Local USB connection</h2>
<h2 style="text-align: left; margin-left: 10px;">{{ _("Local USB connection") }}</h2>
</div>
<p class="note" style="text-align: left; margin-left: 38px;margin-top: 0px;">Select this if Specter is installed on this compter</p>
<p class="note" style="text-align: left; margin-left: 38px;margin-top: 0px;">{{ _("Select this if Specter is installed on this computer") }}</p>
</label>
<br>
<label class="card radio-btn" style="padding: 20px 10px 5px;">
<div class="row">
<input type="radio" name="hwibridge" value="remote" style="width: 20px; min-width: 20px; margin-top: 20px;" onchange="switchHWIBridgeConfView(this.value)" {% if specter.hwi_bridge_url == 'http://127.0.0.1:25441/hwi/api/' %} checked {% endif %}/>
<h2 style="text-align: left; margin-left: 10px;">Remote Specter USB connection</h2>
<h2 style="text-align: left; margin-left: 10px;">{{ _("Remote Specter USB connection") }}</h2>
</div>
<p class="note" style="text-align: left; margin-left: 38px;margin-top: 0px;">Select this if Specter is installed on a different machine</p>
<p class="note" style="text-align: left; margin-left: 38px;margin-top: 0px;">{{ _("Select this if Specter is installed on a different machine") }}</p>
</label>
<br>
<label class="card radio-btn" style="padding: 20px 10px 5px;">
@ -35,7 +35,7 @@
<input type="radio" id="custom_hwibridge" name="hwibridge" value="custom" style="width: 20px; min-width: 20px; margin-top: 20px;" onchange="switchHWIBridgeConfView(this.value)" {% if specter.hwi_bridge_url != 'http://127.0.0.1:25441/hwi/api/' and specter.hwi_bridge_url != '/hwi/api/' %} checked {% endif %}/>
<h2 style="text-align: left; margin-left: 10px;">Custom URL</h2>
</div>
<p class="note" style="text-align: left; margin-left: 38px;margin-top: 0px;">Select this if you would like to configure manually (experts only)</p>
<p class="note" style="text-align: left; margin-left: 38px;margin-top: 0px;">{{ _("Select this if you would like to configure manually (experts only)") }}</p>
<div style="margin: 20px;">
<input type="text" id="custom_url" value="{{ specter.hwi_bridge_url }}" oninput="if(document.getElementById('custom_hwibridge').checked){document.getElementById('hwi_bridge_url').value=this.value}" />
</div>
@ -72,22 +72,22 @@
<div class="card" style="padding: 5px 10px 15px;">
<div class="row">
<img src="{{ url_for('static', filename='img/warning_sign.svg') }}" style="width: 20px;"/>
<p style="color: #FF9A00; font-size: 1.1em; margin-left: 10px;">Before changing these settings:</p>
<p style="color: #FF9A00; font-size: 1.1em; margin-left: 10px;">{{ _("Before changing these settings") }}:</p>
</div>
<p style="margin-left: 30px; margin-top: -10px;">If you havent done so already, head to “Preferences” on your desktop app (found on the top navigation). Select the option to connect remotely.</p>
<p style="margin-left: 30px; margin-top: -10px;">{{ _('If you havent done so already, head to "Preferences" on your desktop app (found on the top navigation). Select the option to connect remotely.') }}</p>
<img src="{{ url_for('static', filename='img/preferences_tab_screenshot.png')}}" width="400px;" style="margin: auto; border: 0px solid #000; display: block;"/>
</div>
<br>
</div>
<br>
<button type="submit" class="btn" value="update_hwi" style="width: 580px;">Save Changes</button>
<button type="submit" class="btn" value="update_hwi" style="width: 580px;">{{ _("Save Changes") }}</button>
</form>
<br>
<p style="text-align: left; width: 580px;">You might also need to allow Specter to communicate with the device bridge.<br></p>
<p style="text-align: left; width: 580px;">{{ _("You might also need to allow Specter to communicate with the device bridge.") }}<br></p>
<ol style="text-align: left; width: 580px;line-height:2;">
<li>Access the device bridge settings. <a href='{{specter.hwi_bridge_url.replace("/hwi/api", "/hwi/settings")}}' target="_blank" style="color: #fff;">Click here</a>.</li>
<li>Add the domain {{ url_for('index', _external=True) }} <span style="text-decoration: underline; cursor: pointer;" onclick="copyText('{{ url_for('index', _external=True) }}', 'Copied Specter URL')">(copy)</span> to the whitelist there.<br>Save the changes and close the window.</li>
<li>Connect your hardware wallet to your computer and test if Specter manage to reach it<br><button class="btn" type="button" style="display: inline; width: 140px; margin-top:5px;" onclick="testHWIConnection()">Test connection</button></li>
<li>{{ _("Access the device bridge settings.") }} <a href='{{specter.hwi_bridge_url.replace("/hwi/api", "/hwi/settings")}}' target="_blank" style="color: #fff;">Click here</a>.</li>
<li>{{ _("Add the domain") }} {{ url_for('index', _external=True) }} <span style="text-decoration: underline; cursor: pointer;" onclick="copyText('{{ url_for('index', _external=True) }}', '{{ _("Copied Specter URL") }}')">({{ _("copy") }}</span> {{ _("to the whitelist there.") }}<br>{{ _("Save the changes and close the window.") }}</li>
<li>{{ _("Connect your hardware wallet to your computer and test if Specter manage to reach it") }}<br><button class="btn" type="button" style="display: inline; width: 140px; margin-top:5px;" onclick="testHWIConnection()">{{ _("Test connection") }}</button></li>
</ol>
<div style="height: 100%"></div>
@ -95,9 +95,9 @@
<script>
async function testHWIConnection() {
if (await enumerate()) {
showNotification('Device detected successfully! USB connections configured correctly!');
showNotification('{{ _("Device detected successfully! USB connections configured correctly!") }}');
} else {
showError('Failed to detect devices');
showError('{{ _("Failed to detect devices") }}');
}
}
function switchHWIBridgeConfView(viewType) {

View file

@ -26,79 +26,79 @@
<br>
<h1>
<img style="width: 25px; margin-right: 7px; vertical-align: middle;" src="{{ url_for('static', filename='img/tor.svg') }}"/><span style="vertical-align: bottom; margin-right: 10px;">
Tor configurations
{{ _("Tor configurations") }}
</h1>
<p class="note center">Enable Tor to anonymise your data connections.</p>
<p class="note center">{{ _("Enable Tor to anonymise your data connections.") }}</p>
<br>
<label for="tor_type_builtin" class="card radio-btn" style="padding: 20px 10px 5px; display: block; margin: auto;">
{% if torbrowser_installed %}
{% if torbrowser_running %}
<span style="display: inline; float: right;margin-top: 10px; margin-right: 10px;">
<span class="note">Running&nbsp;&nbsp;<img src="{{ url_for('static', filename='img/check.svg') }}" class="svg-check-green" style="vertical-align: middle;"/>&nbsp;&nbsp;</span>
<button style="display: inline; width: 80px;" type="submit" class="btn" name="action" value="stoptor">Stop</button>&nbsp;
<button style="display: inline; width: 80px;" type="submit" class="btn" name="action" value="test_tor">Test</button>
<button style="display: inline; width: 80px;" type="submit" class="btn" name="action" value="stoptor">{{ _("Stop") }}</button>&nbsp;
<button style="display: inline; width: 80px;" type="submit" class="btn" name="action" value="test_tor">{{ _("Test") }}</button>
</span>
{% else %}
<span style="display: inline; float: right; margin-top: 10px; margin-right: 10px;">
<span class="note">Stopped&nbsp;&nbsp;</span>
<button style="display: inline; width: 80px;" type="submit" class="btn" name="action" value="uninstalltor">Uninstall</button>&nbsp;
<button style="display: inline; width: 80px;" type="submit" class="btn" name="action" value="starttor">Start</button>
<span class="note">{{ _("Stopped") }}&nbsp;&nbsp;</span>
<button style="display: inline; width: 80px;" type="submit" class="btn" name="action" value="uninstalltor">{{ _("Uninstall") }}</button>&nbsp;
<button style="display: inline; width: 80px;" type="submit" class="btn" name="action" value="starttor">{{ _("Start") }}</button>
</span>
{% endif %}
{% else %}
<a style="display: inline; float: right; width: 110px; margin-top: 10px; margin-right: 10px;" href="{{ url_for('setup_endpoint.tor_from_settings' ) }}" class="btn centered action" id="setup-tor-button">Set up</a>
<a style="display: inline; float: right; width: 110px; margin-top: 10px; margin-right: 10px;" href="{{ url_for('setup_endpoint.tor_from_settings' ) }}" class="btn centered action" id="setup-tor-button">{{ _("Set up") }}</a>
{% endif %}
<div class="row">
<input id="tor_type_builtin" type="radio" name="tor_type" value="builtin" style="width: 20px; min-width: 20px; margin-top: 20px;" onchange="toggletor_type('builtin')" {% if tor_type == 'builtin' %} checked {% endif %}/>
<h2 style="text-align: left; margin-left: 10px; font-size: 1.1em;">Built in</h2>
</div>
<p class="note" style="text-align: left; margin-left: 38px;margin-top: -5px;">Quick and automatic setup.</p>
<p class="note" style="text-align: left; margin-left: 38px;margin-top: -5px;">{{ _("Quick and automatic setup.") }}</p>
</label>
<br>
<label id="custom-tor-label" for="tor_type_custom" class="card radio-btn no-bottom-border-radius" style="padding: 20px 10px 5px; display: block; margin: auto;">
<div class="row">
<input id="tor_type_custom" type="radio" name="tor_type" value="custom" style="width: 20px; min-width: 20px; margin-top: 20px;" onchange="toggletor_type('custom')" {% if tor_type == 'custom' %} checked {% endif %}/>
<h2 style="text-align: left; margin-left: 10px; font-size: 1.1em;">Custom</h2>
<h2 style="text-align: left; margin-left: 10px; font-size: 1.1em;">{{ _("Custom") }}</h2>
</div>
<p class="note" style="text-align: left; margin-left: 38px;margin-top: -5px;">Use your own Tor configuration.</p>
<p class="note" style="text-align: left; margin-left: 38px;margin-top: -5px;">{{ _("Use your own Tor configuration.") }}</p>
</label>
<div id="custom-tor-details" class="card" style="margin: auto;border-top-left-radius: 0px; border-top-right-radius: 0px; border-top: none;">
Use Tor proxy running at:
{{ _("Use Tor proxy running at:") }}
<input id="proxy-url" name="proxy_url" value="{{ proxy_url }}" type="url" placeholder="i.e. socks5://127.0.0.1:9050" />
<br>
<p class="note">
Default for Tor daemon: socks5://127.0.0.1:9050<br>
Default for Tor Browser: socks5://127.0.0.1:9150
{{ _("Default for Tor daemon:") }} socks5://127.0.0.1:9050<br>
{{ _("Default for Tor Browser:") }} socks5://127.0.0.1:9150
</p>
<br>
Tor control port:
{{ _("Tor control port:") }}
<p class="note">
Restart Specter for change of the control port to take effect.
{{ _("Restart Specter for change of the control port to take effect.") }}
</p>
<input name="tor_control_port" value="{{ tor_control_port }}" type="text" placeholder='Set to custom port here, if kept empty the default (9051 and 9151) is used' />
<input name="tor_control_port" value="{{ tor_control_port }}" type="text" placeholder='{{ _("Set to custom port here, if kept empty the default (9051 and 9151) is used") }}' />
<br><br>
<button type="submit" class="btn" name="action" value="test_tor" style="width: 300px;">Test connection</button>
<button type="submit" class="btn" name="action" value="test_tor" style="width: 300px;">{{ _("Test connection") }}</button>
</div>
<br><br>
<h2 style="text-align: center; color: #ccc;">Additional Options</h2>
<h2 style="text-align: center; color: #ccc;">{{ _("Additional Options") }}</h2>
<div style="width: 580px; margin: auto;">
{% if not debug %}
<tool-tip title="Tor hidden service" style="float: right; margin-top: 15px;">
Running Specter behind a Tor hidden service allows you to acccess Specter over Tor from anywhere via a Tor supporting browser.
{{ _("Running Specter behind a Tor hidden service allows you to acccess Specter over Tor from anywhere via a Tor supporting browser.") }}
</tool-tip>
<label class="switch" style="float: right; margin-top: 15px;">
<input type="checkbox" id="hidden_service" name="hidden_service" onchange="checkAuthEabled()"{% if tor_service_id %}checked{% endif %}>
<span class="slider"></span>
</label>
<p style="font-size: 1.1em">Enable Tor hidden service</p>
<p style="font-size: 1.1em">{{ _("Enable Tor hidden service") }}</p>
<p class="note">
Allows access to Specter from anywhere.<br>
Requires authentication to be enabled. <a style="color: var(--cmap-blue);" href="{{ url_for('settings_endpoint.auth' ) }}">Authentication settings</a>
{{ _("Allows access to Specter from anywhere.") }}<br>
{{ _('Requires authentication to be enabled.') }} <a style="color: var(--cmap-blue);" href="{{ url_for('settings_endpoint.auth' ) }}">{{ _("Authentication settings") }}</a>
</p>
{% if tor_service_id %}
<p><b>
Specter hidden service is running at:
<span title="Copy Tor address" style="word-break: break-word;" class="explorer-link" onclick="copyText('http:/\/{{ tor_service_id }}.onion', 'Copied Tor hidden service address: {{ tor_service_id }}.onion')">
{{ _("Specter hidden service is running at:") }}
<span title="Copy Tor address" style="word-break: break-word;" class="explorer-link" onclick="copyText('http:/\/{{ tor_service_id }}.onion', '{{ _("Copied Tor hidden service address:") }} {{ tor_service_id }}.onion')">
{{ tor_service_id }}.onion
</span>
</b></p>
@ -106,20 +106,20 @@
{% endif %}
<br>
<tool-tip title="Tor Only mode" style="float: right; margin-top: 30px;">
Some optional Specter functionalities, like rescanning UTXO on a pruned node, or getting the Bitcoin price, might make calls to external APIs and services.<br><br>
Toggle this on to ensure Specter routes all these external calls over Tor proxy.<br><br>
Note: Some external sources may stop working if they block Tor call.
{{ _("Some optional Specter functionality, like rescanning UTXO on a pruned node or getting the Bitcoin price, might make calls to external APIs and services.") }}<br><br>
{{ _("Toggle this on to ensure Specter routes all these external calls over Tor proxy.") }}<br><br>
{{ _("Note: Some external sources may stop working if they block Tor call.") }}
</tool-tip>
<label class="switch" style="float: right; margin-top: 30px;">
<input type="checkbox" id="only-tor" name="only_tor" {% if only_tor %}checked{% endif %}>
<span class="slider"></span>
</label>
<p style="font-size: 1.1em">Force external calls over Tor?</p>
<p style="font-size: 1.1em">{{ _("Force external calls over Tor?") }}</p>
<p class="note">
Route all external calls over the Tor proxy for improved privacy.
{{ _("Route all external calls over the Tor proxy for improved privacy.") }}
</p>
<br><br>
<button type="submit" class="btn" name="action" value="save" style="width: 100%;">Save changes</button>
<button type="submit" class="btn" name="action" value="save" style="width: 100%;">{{ _("Save changes") }}</button>
<br><br>
</div>
<script type="text/javascript">
@ -139,7 +139,7 @@
function checkAuthEabled() {
{% if specter.config.auth.method == "none" %}
document.getElementById('hidden_service').checked = false;
showError('Please set an authentication method before enabling the hidden service')
showError('{{ _("Please set an authentication method before enabling the hidden service") }}')
{% else %}
return true;
{% endif %}

View file

@ -2,9 +2,9 @@
{% block setup %}
<h1><img src="{{ url_for('static', filename='img/icon.png') }}" style="width: 60px;"/> <img src="{{ url_for('static', filename='img/arrow-right.svg') }}" style="width: 60px;" class="svg-white"/>&nbsp;{{ bitcoin_svg('main', 60) }}<br>
<br>Setup Bitcoin Core</h1><br>
<br>{{ _("Setup Bitcoin Core") }}</h1><br>
<div class="row">
<button type="button" class="btn wizard-btn action" id="setup-bitcoind-button" onclick="setupBitcoind()">Start the Setup!</button>
<button type="button" class="btn wizard-btn action" id="setup-bitcoind-button" onclick="setupBitcoind()">{{ _("Start the Setup!") }}</button>
</div>
{% endblock %}
@ -37,7 +37,7 @@
}
showError(jsonResponse.error, 4000);
} catch(e) {
showError("Failed to download Bitcoin Core...");
showError('{{ _("Failed to download Bitcoin Core...") }}');
showError(e);
}
}

View file

@ -2,108 +2,107 @@
{% block setup %}
<h1><img src="{{ url_for('static', filename='img/icon.png') }}" style="width: 60px;"/> <img src="{{ url_for('static', filename='img/arrow-right.svg') }}" style="width: 60px;" class="svg-white"/>&nbsp;{{ bitcoin_svg(network, 60) }}<br>
<br>Configure your node</h1>
<p id="quicksync-option" {% if network != 'main' %}class="hidden"{% endif %}>QuickSync?&nbsp;&nbsp;
<br>{{ _("Configure your node") }}</h1>
<p id="quicksync-option" {% if network != 'main' %}class="hidden"{% endif %}>{{ _("QuickSync?") }}&nbsp;&nbsp;
<label class="switch" id="quicksync-switch">
<input type="checkbox" name="quicksync" id="quicksync" onchange="toggleNodeTypeSelect()" checked>
<span class="slider"></span>
</label>&nbsp;&nbsp;
<tool-tip title="What is QuickSync?">
QuickSync will set up your new node from a trusted state of the blockchain.<br><br>
This means all block validation will be skipped until the most recent block in the snapshot.<br><br>
Later blocks will be normally validated like with a regular full node.
<tool-tip title='{{ _("What is QuickSync?") }}'>
{{ _("QuickSync will set up your new node from a trusted state of the blockchain.") }}<br><br>
{{ _("This means all block validation will be skipped until the most recent block in the snapshot.") }}<br><br>
{{ _("Later blocks will be normally validated like with a regular full node.") }}
</tool-tip>
</p>
<div {% if network == 'main' %}class="hidden"{% endif %} id="select-node-type-div">
<br>
<h1>Select node type:</h1>
<h1>{{ _("Select node type:") }}</h1>
<div class="row">
<label style="position: relative;">
<input type="radio" name="node-type" id="node-type-archival" class="hidden" value="archival" {% if network != 'main' %}checked{% endif %}>
<div class="small-card radio">
<span style="font-size: 1.2em; font-weight: bold;">Archival Node</span><br>
<span style="font-size: 1.2em; font-weight: bold;">{{ _("Archival Node") }}</span><br>
{% if network == 'main' %}
Requires about 400GB of available storage
{{ _("Requires about 400GB of available storage") }}<br><br>
{% elif network == 'testnet' %}
Requires about 30GB of available storage<br><br>
{{ _("Requires about 30GB of available storage") }}<br><br>
{% else %}
Requires a few MB of available storage<br><br>
{{ _("Requires a few MB of available storage") }}<br><br>
{% endif %}
</div>
</label>
<label style="position: relative;">
<input type="radio" name="node-type" class="hidden" value="pruned" {% if network == 'main' %}checked{% endif %}>
<div class="small-card radio">
<span style="font-size: 1.2em; font-weight: bold;">Pruned Node</span><br>
<span style="font-size: 1.2em; font-weight: bold;">{{ _("Pruned Node") }}</span><br>
{% if network == 'main' %}
Requires about 10GB of available storage<br><br>
{{ _("Requires about 10GB of available storage") }}<br><br>
{% elif network == 'testnet' %}
Requires about 3GB of available storage<br><br>
{{ _("Requires about 3GB of available storage") }}<br><br>
{% else %}
Requires a few MB of available storage<br><br>
{{ _("Requires a few MB of available storage") }}<br><br>
{% endif %}
</div>
</label>
</div>
</div>
<p class="hidden warning" id="helper-text">
Now that you have Bitcoin Core installed, your next step will be to sync with the network.<br><br>
Specter allows you to choose between normal syncing (IBD) - which will download and validate the entire history of the blockchain, or downloading a trusted snapshot of the current state of the network.<br><br>
This snapshot is produced and signed by Specter team - which means you will be trusting it to get the present state of the blockchain, instead of verifying the history yourself.<br><br>
It takes about 5GB to download and will setup a fully synced pruned node for you to use.
You can also disable the QuickSync option, and choose between setting up an archival node (which keeps the entire history) or a pruned node.<br><br>
<b>Please note: it is recommended to use a full archival node, or if storage is a constraint, a pruned node synced from scratch. The option for QuickSync is useful for getting started quickly, but is not recommended in the long term or when using Specter with large amounts.</b>
{{ _("Now that you have Bitcoin Core installed, your next step will be to sync with the network.") }}<br><br>
{{ _("Specter allows you to choose between normal syncing (IBD) - which will download and validate the entire history of the blockchain, or downloading a trusted snapshot of the current state of the network.") }}<br><br>
{{ _("This snapshot is produced and signed by Specter team - which means you will be trusting it to get the present state of the blockchain, instead of verifying the history yourself.") }}<br><br>
{{ _("It takes about 5GB to download and will setup a fully synced pruned node for you to use.") }}<br><br>
{{ _("You can also disable the QuickSync option, and choose between setting up an archival node (which keeps the entire history) or a pruned node.") }}<br><br>
<b>{{ _("Please note: it is recommended to use a full archival node, or if storage is a constraint, a pruned node synced from scratch. The option for QuickSync is useful for getting started quickly, but is not recommended in the long term or when using Specter with large amounts.") }}</b>
</p>
<div class="row center">
<p class="explorer-link" id="helper" onclick="toggleHelper()">Unsure? Click here for more info!</p>
<a class="explorer-link hidden" id="support" href="https://t.me/spectersupport" target="_blank">Still need help?</a>
<p class="explorer-link" id="helper" onclick="toggleHelper()">{{ _("Unsure? Click here for more info!") }}</p>
<a class="explorer-link hidden" id="support" href="https://t.me/spectersupport" target="_blank">{{ _("Still need help?") }}</a>
</div>
<br>
{% if specter.only_tor %}
You have activated to only connect via Tor. This will significantly slow down the download-process and is not recommended.
<br>
{{ _("You have activated to only connect via Tor. This will significantly slow down the download-process and is not recommended.") }}
<br>
{% endif %}
<span id="toggle_advanced" style="cursor: pointer;" onclick="toggleAdvanced()">Advanced &#9654;</span>
<span id="toggle_advanced" style="cursor: pointer;" onclick="toggleAdvanced()">{{ _("Advanced") }} &#9654;</span>
<br>
<br>
<div id="advanced_settings" class="card hidden">
<h2 style="text-decoration: underline;">Advanced configurations</h2><br>
Node Network: <button id="select-network-btn" type="button" onclick="showPageOverlay('switch-network-popup')" class="btn centered">{{ network | title }} (switch)</button>
<h2 style="text-decoration: underline;">{{ _("Advanced configurations") }}</h2><br>
{{ _("Node Network:") }} <button id="select-network-btn" type="button" onclick="showPageOverlay('switch-network-popup')" class="btn centered">{{ network | title }} {{ _("(switch)") }}</button>
<p style="text-align: left;">
Bitcoin Core data directory path:<br>
<input type="text" id="bitcoin_core_datadir" name="bitcoin_core_datadir" type="text" value="" placeholder="default">
{{ _("Bitcoin Core data directory path:") }}<br>
<input type="text" id="bitcoin_core_datadir" name="bitcoin_core_datadir" type="text" value="" placeholder='{{ _("default") }}'>
<br><br>
</p>
</div>
<br>
<div class="row">
<button type="button" class="btn wizard-btn action" id="setup-bitcoind-dir-button" onclick="setupBitcoindDirectory()">Start Syncing!</button>
<button type="button" class="btn wizard-btn action" id="setup-bitcoind-dir-button" onclick="setupBitcoindDirectory()">{{ _("Start Syncing!") }}</button>
</div>
<div id="override-data-folder-popup" class="hidden" style="margin: auto;">
<p>Your Bitcoin Core data folder seems to be already used.<br>
Would you like to override it or select a different folder path?
<p>{{ _("Your Bitcoin Core data folder seems to be already used.<br>Would you like to override it or select a different folder path?") }}
</p><br>
<div class="row">
<button type="button" class="btn" onclick="hidePageOverlay()">Edit path</button>&nbsp;&nbsp;
<button type="button" class="btn" onclick="hidePageOverlay();setupBitcoindDirectory(true)">Override existing</button>
<button type="button" class="btn" onclick="hidePageOverlay()">{{ _("Edit path") }}</button>&nbsp;&nbsp;
<button type="button" class="btn" onclick="hidePageOverlay();setupBitcoindDirectory(true)">{{ _("Override existing") }}</button>
</div>
</div>
<div id="switch-network-popup" class="hidden" style="margin: auto;">
<h1> Select Network to setup </h1>
<h1> {{ _("Select Network to setup") }} </h1>
<div class="row overflow">
<a href="{{ url_for('setup_endpoint.bitcoind_datadir', network='main') }}" style="color: #fff; text-decoration: none;">
<div class="small-card radio" style="min-width: 150px;">
{{ bitcoin_svg('main', 40) }}
<br>
Mainnet
{{ _("Mainnet") }}
</div>
</a>
<a href="{{ url_for('setup_endpoint.bitcoind_datadir', network='testnet') }}" style="color: #fff; text-decoration: none;">
<div class="small-card radio" style="min-width: 150px;">
{{ bitcoin_svg('test', 40) }}
<br>
Testnet
{{ _("Testnet") }}
</div>
</a>
<a href="{{ url_for('setup_endpoint.bitcoind_datadir', network='signet') }}" style="color: #fff; text-decoration: none;">
@ -180,7 +179,7 @@
}
showError(jsonResponse.error, 4000);
} catch(e) {
showError("Failed to start Bitcoin Core...");
showError('{{ _("Failed to start Bitcoin Core...") }}';
showError(e);
}
}

View file

@ -2,7 +2,7 @@
{% block setup %}
<img style="width: 100px;" src="{{ url_for('static', filename='img/party.png') }}"/><br><br>
<h1 style="font-size: 1.8em;">Setup Completed Successfully!</h1>
<p>Your new node is up and running!</p><br>
<a href="{{url_for('about')}}" class="btn wizard-btn action" id="finish-setup-btn">Done</a>
<h1 style="font-size: 1.8em;">{{ _("Setup Completed Successfully!") }}</h1>
<p>{{ _("Your new node is up and running!") }}</p><br>
<a href="{{url_for('about')}}" class="btn wizard-btn action" id="finish-setup-btn">{{ _("Done") }}</a>
{% endblock %}

View file

@ -2,16 +2,16 @@
{% block setup %}
<h1><img src="{{ url_for('static', filename='img/icon.png') }}" style="width: 60px;"/> <img src="{{ url_for('static', filename='img/arrow-right.svg') }}" style="width: 60px;" class="svg-white"/>&nbsp;{{ bitcoin_svg('main', 60) }}<br>
<br>Would you like to setup a new Bitcoin node or connect to an existing one?</h1>
<p class="hidden warning" id="helper-text">If you have an existing Bitcoin Core node you would like to use, either running locally on this computer or somewhere else, like on Umbrel, myNode, or RaspiBlitz, you can just connect and use it.<br><br>
Don't have a node? Specter can help you setup a new one! This guide will help you get it up and running!</p>
<br>{{ _("Would you like to setup a new Bitcoin node or connect to an existing one?") }}</h1>
<p class="hidden warning" id="helper-text">{{ _("If you have an existing Bitcoin Core node you would like to use, either running locally on this computer or somewhere else, like on Umbrel, myNode, or RaspiBlitz, you can just connect and use it.") }}<br><br>
{{ _("Don't have a node? Specter can help you setup a new one! This guide will help you get it up and running!") }}</p>
<div class="row center">
<p class="explorer-link" id="helper" onclick="toggleHelper()">Unsure? Click here for more info!</p>
<a class="explorer-link hidden" id="support" href="https://t.me/spectersupport" target="_blank">Still need help?</a>
<p class="explorer-link" id="helper" onclick="toggleHelper()">{{ _("Unsure? Click here for more info!") }}</p>
<a class="explorer-link hidden" id="support" href="https://t.me/spectersupport" target="_blank">{{ _("Still need help?") }}</a>
</div>
<br><br>
<div class="row">
<a href="{{ url_for('nodes_endpoint.node_settings', node_alias=specter.node.alias) if not specter.node.rpc and specter.node.external_node else url_for('nodes_endpoint.node_settings') }}" class="btn wizard-btn">Connect existing node</a>
<a href="{{ url_for('setup_endpoint.bitcoind' if not bitcoind_installed else 'setup_endpoint.bitcoind_datadir') }}" class="btn wizard-btn action" id="setup-node-btn">Setup a new node</a>
<a href="{{ url_for('nodes_endpoint.node_settings', node_alias=specter.node.alias) if not specter.node.rpc and specter.node.external_node else url_for('nodes_endpoint.node_settings') }}" class="btn wizard-btn">{{ _("Connect existing node") }}</a>
<a href="{{ url_for('setup_endpoint.bitcoind' if not bitcoind_installed else 'setup_endpoint.bitcoind_datadir') }}" class="btn wizard-btn action" id="setup-node-btn">{{ _("Setup a new node") }}</a>
</div>
{% endblock %}

View file

@ -22,9 +22,9 @@
<div class="row">
<form method="POST" action="{{ url_for('about') }}">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" name="action" value="cancelsetup" class="btn hidden danger" id="reset-setup-process" style="max-height: 36px;" onclick="">Cancel process</button>
<button type="submit" name="action" value="cancelsetup" class="btn hidden danger" id="reset-setup-process" style="max-height: 36px;" onclick="">{{ _("Cancel process") }}</button>
</form>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<button type="button" class="btn hidden action" id="show-progress-details" style="max-height: 36px;" onclick="showPageOverlay('progress-details');">View Progress</button>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<button type="button" class="btn hidden action" id="show-progress-details" style="max-height: 36px;" onclick="showPageOverlay('progress-details');">{{ _("View Progress") }}</button>
</div>
<h1 id="progress-details" class="hidden" style="margin: auto;"></h1>
@ -47,7 +47,7 @@
helper.classList.add("helper-btn");
support.classList.add("helper-btn");
} else {
helper.innerText = "Unsure? Click here for more info!";
helper.innerText = '{{ _("Unsure? Click here for more info!") }}';
helperText.classList.add("hidden");
support.classList.add("hidden");
helper.classList.remove("helper-btn");
@ -60,10 +60,10 @@
let advancedSettings = document.getElementById(`advanced_settings`);
if (advancedSettings.classList.contains("hidden")) {
advancedSettings.classList.remove("hidden");
advancedButton.innerHTML = 'Advanced &#9660;';
advancedButton.innerHTML = '{{ _("Advanced") }} &#9660;';
} else {
advancedSettings.classList.add("hidden");
advancedButton.innerHTML = 'Advanced &#9654;';
advancedButton.innerHTML = '{{ _("Advanced") }} &#9654;';
}
}
@ -80,7 +80,7 @@
);
let result = await response.json();
if (result.error) {
showError('Encountered an error:' + result.error);
showError('{{ _("Encountered an error:") }}' + result.error);
document.getElementById('progress-details').classList.add('hidden');
hidePageOverlay('progress-details');
return;
@ -92,7 +92,7 @@
}
document.getElementById('progress-details').innerHTML = `${stage}<br>`;
if (progress > 0 && progress < 100) {
document.getElementById('progress-details').innerHTML += `<br><br><b style="font-size:0.8em;">Download Progress: ${progress}%</b>`;
document.getElementById('progress-details').innerHTML += `<br><br><b style="font-size:0.8em;">{{ _("Download Progress:") }}" ${progress}%</b>`;
}
setTimeout(fetchProgress, 1000);
} catch(e) {
@ -104,7 +104,7 @@
document.getElementById('show-progress-details').classList.remove('hidden');
document.getElementById('reset-setup-process').classList.remove('hidden');
showPageOverlay('progress-details');
document.getElementById('progress-details').innerHTML = 'Starting up...';
document.getElementById('progress-details').innerHTML = '{{ _("Starting up...") }}';
setTimeout(fetchProgress, 1000);
}

View file

@ -1,18 +1,18 @@
{% extends "setup/setup_page.jinja" %}
{% block setup %}
<h1>Specter Quickstart!</h1>
<h2>Get up and running with Specter in just a few clicks!</h2><br>
<h1>{{ _("Specter Quickstart!") }}</h1>
<h2>{{ _("Get up and running with Specter in just a few clicks!") }}</h2><br>
<img style="width: 50px;" src="{{ url_for('static', filename='img/tor.svg') }}"/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
{{ bitcoin_svg('main', 50) }}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<img src="{{ url_for('static', filename='img/icon.png') }}" style="width: 50px;"/>
<br><br>
<p style="max-width: 400px; margin: auto;">
Here, we'll take you through the process of setting up Specter with your own Bitcoin Core node and Tor.<br><br>
Everything is completely automated, but you make choices to customize your setup to fit your needs.
{{ _("Here, we'll take you through the process of setting up Specter with your own Bitcoin Core node and Tor.") }}<br><br>
{{ _("Everything is completely automated, but you make choices to customize your setup to fit your needs.") }}
</p>
<br><br>
<div class="row">
<a href="{{ url_for('setup_endpoint.tor') }}" class="btn wizard-btn action" id="start-setup-btn">Get Started!</a>
<a href="{{ url_for('setup_endpoint.tor') }}" class="btn wizard-btn action" id="start-setup-btn">{{ _("Get Started!") }}</a>
</div>
{% endblock %}

View file

@ -2,13 +2,13 @@
{% block setup %}
<h1><img src="{{ url_for('static', filename='img/icon.png') }}" style="width: 60px;"/> <img src="{{ url_for('static', filename='img/arrow-right.svg') }}" style="width: 60px;" class="svg-white"/>&nbsp;<img style="width: 60px;" src="{{ url_for('static', filename='img/tor.svg') }}"/><br>
<br>Setup Tor daemon</h1>
<br>{{ _("Setup Tor daemon") }}</h1>
<br><br>
<div class="row">
{% if nextURL == 'setup_endpoint.node_type'%}
<a href="{{ url_for('setup_endpoint.node_type') }}" class="btn wizard-btn">Skip</a>
<a href="{{ url_for('setup_endpoint.node_type') }}" class="btn wizard-btn">{{ _("Skip") }}</a>
{% endif %}
<button type="button" class="btn wizard-btn action" id="setup-tor-button" onclick="setupTorDaemon()">Setup Tor</button>
<button type="button" class="btn wizard-btn action" id="setup-tor-button" onclick="setupTorDaemon()">{{ _("Setup Tor") }}</button>
</div>
{% endblock %}
@ -41,7 +41,7 @@
}
showError(jsonResponse.error, 4000);
} catch(e) {
showError("Failed to download Tor daemon...");
showError('{{ _("Failed to download Tor daemon...") }}');
showError(e);
}
}

View file

@ -7,9 +7,9 @@
#}
{% macro editable_label(label, elementId) -%}
<input type="text" class="label" autocomplete="off" spellcheck="false" id="{{'label' ~ elementId }}" name="label" value="{{ label }}" style="border: none; font-size: 1.5em;" disabled />
<button type="submit" class="btn update hidden" id="{{ 'save' ~ elementId }}" name="action" value="updatelabel">Update</button>
<button type="button" class="btn cancel hidden" id="{{ 'cancel' ~ elementId }}" onclick="toggleEdit('{{ elementId }}', '{{ label }}')">Cancel</button>
<button type="button" class="btn edit" id="{{ 'edit' ~ elementId }}" onclick="toggleEdit('{{ elementId }}', '{{ label }}')">Edit Label</button>
<button type="submit" class="btn update hidden" id="{{ 'save' ~ elementId }}" name="action" value="updatelabel">{{ _("Update") }}</button>
<button type="button" class="btn cancel hidden" id="{{ 'cancel' ~ elementId }}" onclick="toggleEdit('{{ elementId }}', '{{ label }}')">{{ _("Cancel") }}</button>
<button type="button" class="btn edit" id="{{ 'edit' ~ elementId }}" onclick="toggleEdit('{{ elementId }}', '{{ label }}')">{{ _("Edit Label") }}</button>
<script>
function toggleEdit(id, originalLabel) {

View file

@ -8,11 +8,11 @@
#}
{% macro wallet_menu(active_menuitem, wallet_alias) -%}
<nav class="row collapse-on-mobile">
{{ wallet_menu_item('history', 'Transactions', wallet_alias, active_menuitem, isLeft=true, id='btn_transactions') }}
{{ wallet_menu_item('addresses', 'Addresses', wallet_alias, active_menuitem) }}
{{ wallet_menu_item('receive', 'Receive', wallet_alias, active_menuitem, id='btn_receive') }}
{{ wallet_menu_item('send', 'Send', wallet_alias, active_menuitem, id='btn_send') }}
{{ wallet_menu_item('settings', 'Settings', wallet_alias, active_menuitem, isRight=true, id='btn_settings') }}
{{ wallet_menu_item('history', _('Transactions'), wallet_alias, active_menuitem, isLeft=true, id='btn_transactions') }}
{{ wallet_menu_item('addresses', _('Addresses'), wallet_alias, active_menuitem) }}
{{ wallet_menu_item('receive', _('Receive'), wallet_alias, active_menuitem, id='btn_receive') }}
{{ wallet_menu_item('send', _('Send'), wallet_alias, active_menuitem, id='btn_send') }}
{{ wallet_menu_item('settings', _('Settings'), wallet_alias, active_menuitem, isRight=true, id='btn_settings') }}
<a href="javascript:void(0);" class="mobile-nav-icon" onclick="toggleMobileNav(this, `{{ url_for('static', filename='img/caret-down.svg') }}`, `{{ url_for('static', filename='img/caret-up.svg') }}`)">
<img style="width: 36px;" src="{{ url_for('static', filename='img/caret-down.svg') }}" class="svg-white"/>
</a>

View file

@ -12,10 +12,10 @@
let useSlip132 = document.getElementById("use_slip_132_pdf").checked;
let currentHeight = 15;
doc.setFontSize(18);
doc.text('Specter Backup File', 105, currentHeight, {align: 'center'});
doc.text('{{ _("Specter Backup File") }}', 105, currentHeight, {align: 'center'});
currentHeight += 10;
doc.setFontSize(14);
doc.text('(keep this information with each of your key backups)', 105, currentHeight, {align: 'center'});
doc.text('{{ _("(keep this information with each of your key backups)") }}', 105, currentHeight, {align: 'center'});
currentHeight += 11;
doc.setFontSize(18);
@ -27,16 +27,16 @@
doc.setFontSize(16);
{% if wallet.is_multisig %}
if ('{{ wallet.address_type }}' == 'bech32') {
doc.text('{{ wallet.sigs_required }} of {{ wallet.keys | length }} multisig (sorted, native segwit)', 105, currentHeight, {align: 'center'});
doc.text('{{ wallet.sigs_required }} of {{ wallet.keys | length }} {{ _("multisig") }} (sorted, native segwit)', 105, currentHeight, {align: 'center'});
} else {
doc.text('{{ wallet.sigs_required }} of {{ wallet.keys | length }} multisig (sorted, nested segwit)', 105, currentHeight, {align: 'center'});
doc.text('{{ wallet.sigs_required }} of {{ wallet.keys | length }} {{ _("multisig") }} (sorted, nested segwit)', 105, currentHeight, {align: 'center'});
}
currentHeight += 7;
{% else %}
if ('{{ wallet.address_type }}' == 'bech32') {
doc.text('(Native Segwit)', 105, currentHeight, {align: 'center'});
doc.text('{{ _("(Native Segwit)") }}', 105, currentHeight, {align: 'center'});
} else {
doc.text('(Nested Segwit)', 105, currentHeight, {align: 'center'});
doc.text('{{ _("(Nested Segwit)") }}', 105, currentHeight, {align: 'center'});
}
currentHeight += 7;
{% endif %}
@ -51,9 +51,9 @@
currentHeight += 110;
doc.setFontSize(16);
doc.text('Scan this QR code with Specter-Desktop', 105, currentHeight, {align: 'center'});
doc.text('{{ _("Scan this QR code with Specter-Desktop") }}', 105, currentHeight, {align: 'center'});
currentHeight += 7;
doc.text('or any other compatible wallet.', 105, currentHeight, {align: 'center'});
doc.text('{{ _("or any other compatible wallet.") }}', 105, currentHeight, {align: 'center'});
currentHeight += 11;
doc.setFontSize(16)
@ -80,11 +80,11 @@
currentHeight += 8
doc.setFont("Roboto", "normal");
doc.text('Device type: ' + deviceType, 15, currentHeight);
doc.text('{{ _("Device type:") }}" ' + deviceType, 15, currentHeight);
currentHeight += 8
doc.text("Master fingerprint: {{ key.fingerprint }}", 15, currentHeight);
doc.text('{{ _("Master fingerprint:") }}" {{ key.fingerprint }}', 15, currentHeight);
currentHeight += 8
doc.text("Derivation path: {{ key.derivation }}", 15, currentHeight);
doc.text('{{ _("Derivation path:") }}" {{ key.derivation }}', 15, currentHeight);
currentHeight += 8
if (useSlip132)
@ -116,11 +116,11 @@
doc.addPage();
currentHeight = 20;
}
doc.text('Advanced Recovery Details', 105, currentHeight, {align: 'center'});
doc.text('{{ _("Advanced Recovery Details") }}', 105, currentHeight, {align: 'center'});
currentHeight += 15;
doc.setFontSize(16);
doc.text('Specter Desktop Raw Data', 105, currentHeight, {align: 'center'});
doc.text('{{ _("Specter Desktop Raw Data") }}', 105, currentHeight, {align: 'center'});
currentHeight += 10;
doc.setFontSize(14)
@ -133,7 +133,7 @@
}
doc.setFontSize(16);
doc.text('Bitcoin Core Receive Descriptor', 105, currentHeight, {align: 'center'});
doc.text('{{ _("Bitcoin Core Receive Descriptor") }}', 105, currentHeight, {align: 'center'});
currentHeight += 10;
doc.setFontSize(14)
@ -146,7 +146,7 @@
}
doc.setFontSize(16);
doc.text('Bitcoin Core Change Descriptor', 105, currentHeight, {align: 'center'});
doc.text('{{ _("Bitcoin Core Change Descriptor") }}', 105, currentHeight, {align: 'center'});
currentHeight += 10;
doc.setFontSize(14)

View file

@ -3,12 +3,11 @@
<form action="./" method="POST" onsubmit="return isValidForm()">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
{% if wallet_type == 'multisig' %}
<h1 style="margin: 20px;">Pick the devices you want in your multisig quorum</h1>
<h1 style="margin: 20px;">{{ _("Pick the devices you want in your multisig quorum") }}</h1>
{% else %}
<h1 style="margin: 20px;">Pick the device you want to use</h1>
<h1 style="margin: 20px;">{{ _("Pick the device you want to use") }}</h1>
{% endif %}
<div class="row overflow">
{# Disable BitBox02 multisig for now... #}
{% for device_name in specter.device_manager.devices_names %}
{% set device = specter.device_manager.devices[device_name] %}
<label style="position: relative;">
@ -27,18 +26,18 @@
<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
{{ _("Add new device") }}
</div>
</a>
</div>
<button id="submit-device" type="submit" name="action" value="device" class="btn centered">Continue</button>
<button id="submit-device" type="submit" name="action" value="device" class="btn centered">{{ _("Continue") }}</button>
<br>
<div class="note">
<center>
{% if wallet_type == 'multisig' %}
We use sorted multisig (BIP-67), so <b>order is NOT important.</b><br>
{{ _("We use sorted multisig (BIP-67), so <b>order is NOT important.</b>") }}<br>
<br>
Be aware of <a style="color: #fff" href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/multisig-security-tradeoffs.md" target="_blank">multisig security tradeoffs</a>.<br>
{{ _('Be aware of <a style="color: #fff" href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/multisig-security-tradeoffs.md" target="_blank">multisig security tradeoffs</a>.') }}<br>
<br>
{% endif %}
</center>

View file

@ -2,36 +2,36 @@
{% block main %}
<form id="keysform" action="./" method="POST" onsubmit="showPacman()" class="table-holder">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<h1>Name your Wallet</h1>
<h1>{{ _("Name your Wallet") }}</h1>
<div class="center">
<input type="text" id="wallet_name" name="wallet_name" class="inline" value="{{ wallet_name }}" placeholder="My new wallet" required>
</div>
<br>
<h1>Type of the wallet</h1>
<h1>{{ _("Type of the wallet") }}</h1>
<nav class="row">
<label>
<input type="radio" name="type" value="{{'sh-wsh' if wallet_type == 'multisig' else 'sh-wpkh'}}"
class="hidden" onchange="updateType(this.value);">
<div class="btn radio left">Nested Segwit</div>
<div class="btn radio left">{{ _("Nested Segwit") }}</div>
</label>
<label>
<input type="radio" name="type" value="{{'wsh' if wallet_type == 'multisig' else 'wpkh'}}" checked
class="hidden" onchange="updateType(this.value);">
<div class="btn radio right">Segwit</div>
<div class="btn radio right">{{ _("Segwit") }}</div>
</label>
</nav>
<div class="note optional">
<center>
<br><b>Segwit</b> uses bech32-encoded addresses (bc1), <b>Nested Segwit</b> makes it compatible with legacy software. Don't use legacy.
<br>{{ _("<b>Segwit</b> uses bech32-encoded addresses (bc1), <b>Nested Segwit</b> makes it compatible with legacy software. Don't use legacy.") }}
</center>
</div>
<br>
{% if wallet_type == 'multisig' %}
<div class="center">
Using
{{ _("Using") }}
<input class="inline" type="number" name="sigs_required" min=1 max="{{ cosigners | length }}" step=1 value="{{ sigs_required }}" />
of
{{ _("of") }}
<span style="margin: 0px 40px; font-size: 1.2em;">{{ cosigners | length }}<span>
</div>
<br>
@ -39,13 +39,13 @@
<div class="center" style="max-width: 500px; margin: auto;">
<div style="display: inline-block; text-align: center; margin-top: 10px;">
<input form="keysform" type="checkbox" name="rescanblockchain" id="rescan" class="inline" onchange="toggleScanOptions(this)">
<label for="rescan">Scan for existing funds?</label>
<label for="rescan">{{ _("Scan for existing funds?") }}</label>
<div class="tool-tip">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Importing an existing wallet<br></span>
Check this option to scan the blockchain for existing wallet balance and history.
<span class="info__title">{{ _("Importing an existing wallet") }}<br></span>
{{ _("Check this option to scan the blockchain for existing wallet balance and history.") }}
</span>
</p>
</div>
@ -53,16 +53,16 @@
<div id="scan-options" class="hidden">
<br>
<div>
Rescan type:
<label><input type="radio" class="inline" style="margin: 0 10px 0 20px;" name="full_rescan_option" value="full" onclick="toggleFullRescanOption(this)" checked>Full rescan</label>
<label><input type="radio" class="inline" style="margin: 0 10px 0 20px" name="full_rescan_option" value="utxo" onclick="toggleFullRescanOption(this);">UTXO only</label>
{{ _("Rescan type:") }}
<label><input type="radio" class="inline" style="margin: 0 10px 0 20px;" name="full_rescan_option" value="full" onclick="toggleFullRescanOption(this)" checked>{{ _("Full rescan") }}</label>
<label><input type="radio" class="inline" style="margin: 0 10px 0 20px" name="full_rescan_option" value="utxo" onclick="toggleFullRescanOption(this);">{{ _("UTXO only") }}</label>
<div class="tool-tip">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Choosing Rescan Type<br></span>
Full rescan is a relativly slow process which looks for and imports the full wallet transaction history and balance.<br><br>
UTXO only is a fast rescan option which only looks for existing balances (UTXO), but will not import the full transaction history.
<span class="info__title">{{ _("Choosing Rescan Type") }}<br></span>
{{ _("Full rescan is a relativly slow process which looks for and imports the full wallet transaction history and balance.") }}<br><br>
{{ _("UTXO only is a fast rescan option which only looks for existing balances (UTXO), but will not import the full transaction history.") }}
</span>
</p>
</div>
@ -74,32 +74,32 @@
{% else %}
{% set startblock=(0 if not specter.info['pruned'] else specter.info['pruneheight']) %}
{% endif %}
Rescan blockchain from block: <input type="number" name="startblock" value="{{startblock}}" min="{{startblock}}" style="max-width: 150px; margin-left: 5px;">
{{ _("Rescan blockchain from block:") }} <input type="number" name="startblock" value="{{startblock}}" min="{{startblock}}" style="max-width: 150px; margin-left: 5px;">
{% if specter.info['pruned'] %}
<span class="warning" style="display: inline-block; max-width:300px;">
<img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>You are using a pruned node.<br>Wallet history may not fully appear.<br>Rescan is limited by the pruned height to block: {{specter.info['pruneheight']}}
<img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>{{ _("You are using a pruned node.<br>Wallet history may not fully appear.<br>Rescan is limited by the pruned height to block:") }} {{specter.info['pruneheight']}}
</span>
{% endif %}
<div class="note center" style="margin-top: 5px; line-height: 2;">In case you're not sure, it is best to just leave the default number.<br><b>481824</b> was the first Segwit block.</div>
<div class="note center" style="margin-top: 5px; line-height: 2;">{{ _("In case you're not sure, it is best to just leave the default number.<br><b>481824</b> was the first Segwit block.") }}</div>
<br>
</div>
<div id="utxo-scan-options" style="display: none">
{% if specter.info['pruned'] %}
<label for="use_explorer"><input style="width: auto; min-width: auto;" type="checkbox" id="use_explorer" name="use_explorer" checked> fetch missing data from block explorer
<tool-tip title="Fetch missing data">
You are using a pruned node. Blocks for some transactions may be missing.<br><br>
Check this checkbox if you want to fetch missing information from block explorer.
<label for="use_explorer"><input style="width: auto; min-width: auto;" type="checkbox" id="use_explorer" name="use_explorer" checked> {{ _("fetch missing data from block explorer") }}
<tool-tip title="{{ _('Fetch missing data') }}">
{{ _("You are using a pruned node. Blocks for some transactions may be missing.") }}<br><br>
{{ _("Check this checkbox if you want to fetch missing information from block explorer.") }}
</tool-tip>
</label>
<div id="utxo-explorer">
<br>
Block explorer URL:
{{ _("Block explorer URL:") }}
{% include "components/explorer_select.jinja" %}
<span class="warning" id="utxo-warning" style="display: inline-block; max-width:300px;">
<img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>Careful with block explorers!<br>
This may have privacy implication!<br>
Information fetched from the block explorer contains a list of unspent transactions of your wallet from old (pruned) blocks.<br>
"They" will know what transactions you are interested in.
<img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>{{ _("Careful with block explorers!") }}<br>
{{ _("This may have privacy implications!") }}<br>
{{ _("Information fetched from the block explorer contains a list of unspent transactions of your wallet from old (pruned) blocks.") }}<br>
{{ _('"They" will know what transactions you are interested in.') }}
</span>
</div>
{% endif %}
@ -112,18 +112,18 @@
{% endfor %}
<input type="hidden" name="sigs_total" value="{{ cosigners | length }}">
{% for device in cosigners %}
<h1>Signer {{ loop.index }} - {{ device.name }}</h1>
<h1>{{ _("Signer") }} {{ loop.index }} - {{ device.name }}</h1>
{% set outer_loop = loop %}
{% if device.keys | length > 0 %}
<div class="table-holder">
<table>
<thead>
<tr>
<th>Network</th>
<th>Purpose</th>
<th class="optional">Derivation</th>
<th class="optional">Key</th>
<th>Actions</th>
<th>{{ _("Network") }}</th>
<th>{{ _("Purpose") }}</th>
<th class="optional">{{ _("Derivation") }}</th>
<th class="optional">{{ _("Key") }}</th>
<th>{{ _("Actions") }}</th>
</tr>
</thead>
<tbody id="device{{ loop.index0 }}">
@ -138,17 +138,17 @@
<td width="120px">
<label>
<input class="device-key hidden" data-key-types="{{key.key_type}}" type="radio" name="key{{ outer_loop.index0 }}" value="{{ key.original }}" {% if loop.index == 1 %}checked{% endif %}/>
<div class="btn inline hovering">Use this key</div>
<div class="btn inline hovering">{{ _("Use this key") }}</div>
</label>
</td>
</tr>
{% endfor %}
<tr class="hidden">
<td>
The device does not have keys matching the wallet type selected.
{{ _("The device does not have keys matching the wallet type selected.") }}
</td>
<td>
Please choose a different type or add the keys to the device...
{{ _("Please choose a different type or add the keys to the device...") }}
</td>
<td>
</td>
@ -161,11 +161,11 @@
</table>
</div>
{% else %}
<div class="center">Looks like this device doesn't have keys matching this type of wallet.</div>
<div class="center">{{ _("Looks like this device doesn't have keys matching this type of wallet.") }}</div>
{% endif %}
<div class="spacer"></div>
{% endfor %}
<button type="submit" name="action" value="key" class="btn centered action">Create wallet</button>
<button type="submit" name="action" value="key" class="btn centered action">{{ _("Create wallet") }}</button>
</form>
<div class="spacer"></div>
{% endblock %}

View file

@ -1,20 +1,20 @@
{% extends "base.jinja" %}
{% block main %}
<div class="spacer"></div>
<h1>Select the type of the wallet</h1>
<h1>{{ _("Select the type of the wallet") }}</h1>
<div>
<center>
<span>Adding a wallet with existing funds?</span>
<span>{{ _("Adding a wallet with existing funds?") }}</span>
<div class="tool-tip">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Importing an existing wallet?<br></span>
After choosing device(s), click continue, then just select the:<br> "Scan for existing funds" checkbox on the next screen.
<span class="info__title">{{ _("Importing an existing wallet?") }}<br></span>
{{ _('After choosing device(s), click continue, then just select the:<br> "Scan for existing funds" checkbox on the next screen.') }}
<br>
{% if specter.info["pruned"] %}
<span class="warning" style="display: inline-block;"><img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>
Please Note:<br>You are using a pruned node. Wallet history may not fully appear.
{{ _("Please Note:<br>You are using a pruned node. Wallet history may not fully appear.") }}
</span>
{% endif %}
</span>
@ -25,26 +25,26 @@
<nav class="row overflow">
<a href="./simple/" class="small-card">
<img src="{{ url_for('static', filename='img/key.svg') }}" class="svg-white" style="width: 120px;"/>
Single key wallet
{{ _("Single key wallet") }}
</a>
<a class="small-card" href="./multisig/">
<img src="{{ url_for('static', filename='img/two-keys.svg') }}" class="svg-white" style="width: 120px;"/>
Multisignature wallet<br><br>
{{ _("Multisignature wallet") }}<br><br>
</a>
</nav>
<div class="raw">
<button onclick="showPageOverlay('account_map_import')" type="button" class="btn centered padded">
Import from wallet software
{{ _("Import from wallet software") }}
<div class="tool-tip">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Import From Wallet Software<br> <br></span>
Click here to import an existing wallet from a wallet software like Electrum, Fully-Noded, Sparrow Wallet, or another Specter instance.
<span class="info__title">{{ _("Import From Wallet Software") }}<br> <br></span>
{{ _("Click here to import an existing wallet from a wallet software like Electrum, Fully-Noded, Sparrow Wallet, or another Specter instance.") }}
<br>
{% if specter.info["pruned"] %}
<span class="warning" style="display: inline-block;"><img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>
Please Note:<br>You are using a pruned node. Wallet history may not fully appear.
{{ _("Please Note:<br>You are using a pruned node. Wallet history may not fully appear.") }}
</span>
{% endif %}
</span>
@ -54,6 +54,6 @@
</div>
{% include "wallet/new_wallet/components/import_wallet_account_map.jinja" %}
<div class="note">
<center>Multisig is better for large-ish amounts.</center>
<center>{{ _("Multisig is better for large-ish amounts.") }}</center>
</div>
{% endblock %}

View file

@ -37,28 +37,27 @@
<address-label data-address="{{ wallet.address }}" data-label="{{ wallet.getlabel(wallet.address) }}" data-wallet="{{ wallet_alias }}" date-size="title"></address-label>
</form>
<div class="warning" style="font-weight: lighter; background: #0003; color: #ddd; display: inline-block; margin:5% auto 5%; border: 1px solid #777; padding: 20px; max-width: 100%;">
<p title="Click to copy" id="address" class="padded" style="word-break: break-all;" onclick="copyText('{{ wallet.address }}', 'Copied Address: {{ wallet.address }}');">{{ wallet.address }}</p>
(click to copy)
<p title="Click to copy" id="address" class="padded" style="word-break: break-all;" onclick="copyText('{{ wallet.address }}', '{{ _("Copied Address:") }} {{ wallet.address }}');">{{ wallet.address }}</p>
{{ _("(click to copy)") }}
<br><br>
<qr-code style="margin: auto;" value="{{'bitcoin:' + wallet.address }}" width="256"></qr-code><br>
</div><br>
<form action="./" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" name="action" value="newaddress" class="btn centered" style="margin-bottom: 15px">Get new address</button>
<button type="submit" name="action" value="newaddress" class="btn centered" style="margin-bottom: 15px">{{ _("Get new address") }}</button>
{% set supports_qr_code_verify = [] %}
{% for device in wallet.devices if device.qr_code_support_verify %}
{% set supports_qr_code_verify = supports_qr_code_verify.append(device) %}
{% endfor %}
{% if supports_qr_code_verify != [] %}
<div id="verify_address_qr_code" class="hidden">
<h1>Scan to Verify Address on Your Device</h1><br>
<h1>{{ _("Scan to Verify Address on Your Device") }}</h1><br>
<div id="verify_address_qr_code_container"></div><br>
<p>
Specter can verify this address if you scan it.<br>
It has an address index included in the QR code.
{{ _("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="btn centered padded">{{ _("Verify address via QR code") }}</button>
{% endif %}
{% set supports_hwi = [] %}
{% set supports_hwi_multisig_display_address = [] %}
@ -69,9 +68,9 @@
{% set supports_hwi = supports_hwi.append(device) %}
{% endfor %}
{% if supports_hwi != [] and (supports_hwi_multisig_display_address != [] or not wallet.is_multisig) %}
<button type="button" onclick='displayAddressOnDevice(`{{ wallet.address }}`, `{{ wallet.get_descriptor()|tojson }}`)' class="btn centered optional">Display address on device</button><br>
<button type="button" onclick='displayAddressOnDevice(`{{ wallet.address }}`, `{{ wallet.get_descriptor()|tojson }}`)' class="btn centered optional">{{ _("Display address on device") }}</button><br>
{% if wallet.is_multisig %}
<p class="note center optional">Multsig address on-device display is only available for BitBox02, ColdCard, KeepKey, Specter, and Trezor devices.</p>
<p class="note center optional">{{ _("Multsig address on-device display is only available for BitBox02, ColdCard, KeepKey, Specter, and Trezor devices.") }}</p>
{% endif %}
{% endif %}
</form>
@ -84,10 +83,10 @@
{% set supports_export_to_device = supports_export_to_device.append(device) %}
{% endfor %}
{% if supports_export_to_device != [] %}
<h1>Export To Device</h1>
<p style="max-width: 500px;">Some of your devices require that you first import your multisig wallet data into the device before you can start verifying addresses and sending transactions.</p>
<h1>{{ _("Export To Device") }}</h1>
<p style="max-width: 500px;">{{ _("Some of your devices require that you first import your multisig wallet data into the device before you can start verifying addresses and sending transactions.") }}</p>
<div class="note center">
Import this wallet to a device by scanning its QR code or importing its data file.
{{ _("Import this wallet to a device by scanning its QR code or importing its data file.") }}
</div>
{% for device in wallet.devices %}
@ -97,60 +96,60 @@
download="{{ wallet.name | ascii20 }}.txt"
href="data:text/plain;charset=US-ASCII,{{ device.export_wallet(wallet) }}"
{% if device.device_type == 'coldcard' %}
onclick="showNotification('Import wallet file to your ColdCard: Settings -> Multisig Wallets -> Import from SD', 0)"
onclick="showNotification('{{ _("Import wallet file to your ColdCard: Settings -> Multisig Wallets -> Import from SD") }}', 0)"
{% endif %}
class="btn centered padded">
Save {{ device.name }} file
{{ _("Save") }} {{ device.name }} {{ _("file") }}
</a>
{% elif device.wallet_export_type == 'qr' %}
<button onclick="hidePageOverlay();showPageOverlay('{{ device.alias }}_export_qr_code')" type="button" class="btn centered padded">Show {{ device.name }} QR Code</button>
<button onclick="hidePageOverlay();showPageOverlay('{{ device.alias }}_export_qr_code')" type="button" class="btn centered padded">{{ _("Show") }} {{ device.name }} {{ _("QR Code") }}</button>
<div id="{{ device.alias }}_export_qr_code" style="display: none;">
<h1>Scan this with your {{ device.name }} device </h1>
<h1>{{ _("Scan this with your") }} {{ device.name }} "{{ _("device") }} </h1>
<qr-code value="{{ device.export_wallet(wallet) }}" width="400" scalable></qr-code>
<br>
<button onclick="hidePageOverlay();showPageOverlay('new_wallet_devices_popup')" type="button" class="btn centered padded">Back</button>
<button onclick="hidePageOverlay();showPageOverlay('new_wallet_devices_popup')" type="button" class="btn centered padded">{{ _("Back") }}</button>
</div>
{% endif %}
{% endif %}
{% endfor %}
<br>
{% endif %}
<p style="max-width: 500px;">You can always do this later from the wallet Settings tab.</p>
<button type="button" class="btn centered padded" onclick="hidePageOverlay()">Finish</button>
<p style="max-width: 500px;">{{ _("You can always do this later from the wallet Settings tab.") }}</p>
<button type="button" class="btn centered padded" onclick="hidePageOverlay()">{{ _("Finish") }}</button>
</div>
<div class="hidden" id="new_wallet_popup">
<h1>New wallet was created successfully!</h1>
<h1>{{ _("New wallet was created successfully!") }}</h1>
<button id="pdf-wallet-download" class="btn centered action" type="button">
<img src="{{ url_for('static', filename='img/file.svg') }}" style="width: 30px; margin-right: 2px;" class="svg-white">
Save Backup PDF
{{ _("Save Backup PDF") }}
</button>
{% include 'wallet/components/wallet_pdf.jinja' %}
<br>
<span id="advanced_pdf_export_toggle" style="cursor: pointer;" onclick="togglePDFExportAdvanced()">Advanced &#9654;</span>
<span id="advanced_pdf_export_toggle" style="cursor: pointer;" onclick="togglePDFExportAdvanced()">{{ _("Advanced") }} &#9654;</span>
<br>
<br>
<div style="margin: 0 auto;" class="center hidden" id="advanced_pdf_export_container">
<label style="font-size: 1em;"">Use SLIP-132:&nbsp;</label>
<label style="font-size: 1em;"">{{ _("Use SLIP-132:") }}&nbsp;</label>
<label class="switch">
<input type="checkbox" id="use_slip_132_pdf" style="margin: auto;" onchange="toggleKeyDisplay()">
<span class="slider"></span>
</label><br><br>
</div>
<p class="note center" style="margin-top: 5px; max-width: 90%;">To format master keys on the PDF backup document.</p>
<p class="note center" style="margin-top: 5px; max-width: 90%;">{{ _("To format master keys on the PDF backup document.") }}</p>
<p style="max-width: 500px;">
It is recommended that you print and save this wallet backup file with each of your devices.
{{ _("It is recommended that you print and save this wallet backup file with each of your devices.") }}
</p>
<p style="max-width: 500px;" class="note">
This backup includes all the information you'd need to restore your wallet in Specter Desktop or other compatible wallets including Bitcoin Core itself.<br>
{{ _("This backup includes all the information you'd need to restore your wallet in Specter Desktop or other compatible wallets including Bitcoin Core itself.") }}<br>
<p>
<p style="max-width: 500px;" class="note">
It is recommended that you keep this file private, as it contains all the information needed to track all of the wallet balance and transactions history.
{{ _("It is recommended that you keep this file private, as it contains all the information needed to track all of the wallet balance and transactions history.") }}
<p>
<p style="max-width: 500px;">You can always download this file later from the wallet Settings tab.</p>
<p style="max-width: 500px;">{{ _("You can always download this file later from the wallet Settings tab.") }}</p>
{% if wallet.is_multisig and supports_export_to_device != [] %}
<button type="button" class="btn centered padded" onclick="hidePageOverlay();showPageOverlay('new_wallet_devices_popup')">Continue</button>
<button type="button" class="btn centered padded" onclick="hidePageOverlay();showPageOverlay('new_wallet_devices_popup')">{{ _("Continue") }}</button>
{% else %}
<button type="button" class="btn centered padded" onclick="hidePageOverlay()" id="btn_continue">Continue</button>
<button type="button" class="btn centered padded" onclick="hidePageOverlay()" id="btn_continue">{{ _("Continue") }}</button>
{% endif %}
</div>
<script>
@ -171,10 +170,10 @@
let advancedSettings = document.getElementById(`advanced_pdf_export_container`);
if (advancedSettings.classList.contains("hidden")) {
advancedSettings.classList.remove("hidden");
advancedButton.innerHTML = 'Advanced &#9660;';
advancedButton.innerHTML = '{{ _("Advanced") }} &#9660;';
} else {
advancedSettings.classList.add("hidden");
advancedButton.innerHTML = 'Advanced &#9654;';
advancedButton.innerHTML = '{{ _("Advanced") }} &#9654;';
}
}

View file

@ -9,9 +9,9 @@
{% macro send_nav(active_menuitem, wallet_alias) -%}
<br>
<nav class="row collapse-on-mobile">
{{ wallet_menu_item('send_new', 'New', wallet_alias, active_menuitem, isLeft=true) }}
{{ wallet_menu_item('send_pending', 'Unsigned', wallet_alias, active_menuitem) }}
{{ wallet_menu_item('import_psbt', 'Import', wallet_alias, active_menuitem, isRight=true) }}
{{ wallet_menu_item('send_new', _('New'), wallet_alias, active_menuitem, isLeft=true) }}
{{ wallet_menu_item('send_pending', _('Unsigned'), wallet_alias, active_menuitem) }}
{{ wallet_menu_item('import_psbt', _('Import'), wallet_alias, active_menuitem, isRight=true) }}
<a href="javascript:void(0);" class="mobile-nav-icon" onclick="toggleMobileNav(this, `{{ url_for('static', filename='img/caret-down.svg') }}`, `{{ url_for('static', filename='img/caret-up.svg') }}`)">
<img style="width: 36px;" src="{{ url_for('static', filename='img/caret-down.svg') }}" class="svg-white"/>
</a>

View file

@ -4,24 +4,24 @@
{% include "includes/qr-scanner.html" %}
{% from 'wallet/send/components/send_nav.jinja' import send_nav %}
{{ send_nav('import_psbt', wallet_alias) }}
<h1 class="padded">Import PSBT transaction:</h1>
<span class="center note padded">Paste base64-encoded PSBT transaction here to import.</span>
<h1 class="padded">{{ _("Import PSBT transaction:") }}</h1>
<span class="center note padded">{{ _("Paste base64-encoded PSBT transaction here to import.") }}</span>
<form action="{{ url_for('wallets_endpoint.import_psbt',wallet_alias=wallet_alias) }}" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="card">
<div class="row">
<textarea name="rawpsbt" id="rawpsbt" placeholder="Paste PSBT here"></textarea>
<textarea name="rawpsbt" id="rawpsbt" placeholder="{{ _('Paste PSBT here') }}"></textarea>
</div>
<div class="row break-row-mobile">
<qr-scanner id="psbt-scan" style="width:50%">
<a slot="button" href="#" class="btn" style="margin: 8px;">
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 26px; margin: 0px;" class="svg-white"> Scan
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 26px; margin: 0px;" class="svg-white"> {{ _("Scan") }}
<div class="tool-tip">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Using an airgapped device with QR codes<br><br></span>
PSBT should be base64 encoded.
<span class="info__title">{{ _("Using an airgapped device with QR codes") }}<br><br></span>
{{ _("PSBT should be base64 encoded.") }}
</span>
</p>
</div>
@ -29,19 +29,19 @@
</qr-scanner>
<input type="file" id="file" class="inputfile"/>
<label for="file" class="btn" style="margin: 8px;">
<img src="{{ url_for('static', filename='img/file.svg') }}" style="width: 26px; margin: 0px;" class="svg-white"> Choose file
<img src="{{ url_for('static', filename='img/file.svg') }}" style="width: 26px; margin: 0px;" class="svg-white"> {{ _("Choose file") }}
<div class="tool-tip">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Using a file<br><br></span>
The file should contain either base64 or binary encoded PSBT.
<span class="info__title">{{ _("Using a file") }}<br><br></span>
{{ _("The file should contain either base64 or binary encoded PSBT.") }}
</span>
</p>
</div>
</label>
</div>
<button type="submit" name="action" value="importpsbt" class="btn centered" style="margin-top: 20px;">Import transaction</button>
<button type="submit" name="action" value="importpsbt" class="btn centered" style="margin-top: 20px;">{{ _("Import transaction") }}</button>
</div>
</form>
{% endblock %}

View file

@ -12,7 +12,7 @@
<table style="table-layout: fixed; display: {% if selected_coins %}block{% else %}none{% endif %}; max-width:98%;" id="coin_selection_table">
<thead>
<tr>
<th></th><th>TxID</th><th>Address</th><th>Amount</th>
<th></th><th>{{ _("TxID") }}</th><th>{{ _("Address") }}</th><th>{{ _("Amount") }}</th>
</tr>
</thead>
<tbody>

View file

@ -2,31 +2,31 @@
<div class="fee_container">
<div>
<label><input type="checkbox" class="inline" name="subtract" id="subtract" {% if subtract %}checked{% endif %} onchange="toggleSubtractFrom(this)"> Subtract fees from amount</label>
<label><input type="checkbox" class="inline" name="subtract" id="subtract" {% if subtract %}checked{% endif %} onchange="toggleSubtractFrom(this)"> {{ _("Subtract fees from amount") }}</label>
<div class="tool-tip" style="text-align: center;">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Subtract fees from amount<br><br></span>
If checked, the transaction fees will be paid off from the transaction amount.<br><br>
Otherwise, the fees will be paid as an added cost in addition to the amount sent.
<span class="info__title">{{ _("Subtract fees from amount") }}<br><br></span>
{{ _("If checked, the transaction fees will be paid off from the transaction amount.") }}<br><br>
{{ _("Otherwise, the fees will be paid as an added cost in addition to the amount sent.") }}
</span>
</p>
</div>
</div>
<span id="subtract_from" class="hidden"><br>Subtract from recipient number: <input id="subtract_from_input" name="subtract_from" type="number" min="1" value="{{ subtract_from }}" step="1" style="width: 80px; min-width: 80px;"><br></span>
<span id="subtract_from" class="hidden"><br>{{ _("Subtract from recipient number:") }} <input id="subtract_from_input" name="subtract_from" type="number" min="1" value="{{ subtract_from }}" step="1" style="width: 80px; min-width: 80px;"><br></span>
<br>
<div>
Fees:
<label><input type="radio" class="inline" style="margin: 0 10px 0 20px;" id="fee_options_dynamic" name="fee_options" value="dynamic" onclick="showFeeOption(this)" {% if fee_options_dynamic %}checked{% endif %}>dynamic</label>
<label><input type="radio" class="inline" style="margin: 0 10px 0 20px" id="fee_option_manual" name="fee_options" value="manual" onclick="showFeeOption(this);" {% if not fee_options_dynamic %}checked{% endif %}>manual</label>
{{ _("Fees:") }}
<label><input type="radio" class="inline" style="margin: 0 10px 0 20px;" id="fee_options_dynamic" name="fee_options" value="dynamic" onclick="showFeeOption(this)" {% if fee_options_dynamic %}checked{% endif %}>{{ _("dynamic") }}</label>
<label><input type="radio" class="inline" style="margin: 0 10px 0 20px" id="fee_option_manual" name="fee_options" value="manual" onclick="showFeeOption(this);" {% if not fee_options_dynamic %}checked{% endif %}>{{ _("manual") }}</label>
</div>
<br>
<div id = "fee_manual" style="display: {% if fee_options_dynamic %}none{% else %}block{% endif %}">
Fee rate:<br>
{{ _("Fee rate:") }}<br>
<input type="number" class="fee_rate" name="fee_rate" id="fee_rate" min="1" step="any" autocomplete="off" {% if not fee_options_dynamic %}value="{{ fee_rate }}"{% endif %}> sat/vbyte
<div class="note">
leave blank to set automatically, 1 sat/vbyte is the minimal fee rate.
{{ _("leave blank to set automatically, 1 sat/vbyte is the minimal fee rate.") }}
</div>
</div>
<div id ="fee_dynamic" style="display: {% if fee_options_dynamic %}block{% else %}none{% endif %}">
@ -34,13 +34,13 @@
<input type="range" style="width: 12em" min="{{fee_estimation_data['minimumFee']}}" max="{{fee_estimation_data['fastestFee'] * 1.4}}" value="{{ fee_estimation }}" step="1" id="fees_slider" oninput="dynamicFeeUpdated()">
<input type="hidden" id="fee_rate_dynamic" name="fee_rate_dynamic" value="0">
<div>
Estimated speed: <span id="fee_rate_speed_text"></span>
{{ _("Estimated speed:") }} <span id="fee_rate_speed_text"></span>
<br>
<span class="note">(Fee rate: <span id="fee_rate_dynamic_text"></span> sat/vbyte)</span>
<span class="note">({{ _("Fee rate:") }} <span id="fee_rate_dynamic_text"></span> sat/vbyte)</span>
</div>
</div>
<br>
<label><input type="checkbox" class="inline" name="rbf" id="rbf" {% if rbf %}checked{% endif %}> RBF Enabled</label>
<label><input type="checkbox" class="inline" name="rbf" id="rbf" {% if rbf %}checked{% endif %}> {{ _("RBF Enabled") }}</label>
</div>
<script>
@ -66,17 +66,17 @@
let halfHourFee = parseInt("{{fee_estimation_data['halfHourFee']}}");
let fastestFee = parseInt("{{fee_estimation_data['fastestFee']}}");
if (feesSlider.value <= minFee + ((hourFee - minFee) / 2)) {
document.getElementById('fee_rate_speed_text').innerText = 'Very slow';
document.getElementById('fee_rate_speed_text').innerText = '{{ _("Very slow") }}';
} else if (feesSlider.value > minFee + ((hourFee - minFee) / 2) && feesSlider.value < hourFee) {
document.getElementById('fee_rate_speed_text').innerText = 'Slow';
document.getElementById('fee_rate_speed_text').innerText = '{{ _("Slow") }}';
} else if (feesSlider.value >= hourFee && feesSlider.value < halfHourFee) {
document.getElementById('fee_rate_speed_text').innerText = 'Medium (1 hour)';
document.getElementById('fee_rate_speed_text').innerText = '{{ _("Medium (1 hour)") }}';
} else if (feesSlider.value >= halfHourFee && feesSlider.value < fastestFee) {
document.getElementById('fee_rate_speed_text').innerText = 'Fast (30 minutes)';
document.getElementById('fee_rate_speed_text').innerText = '{{ _("Fast (30 minutes)") }}';
} else if (feesSlider.value >= fastestFee && feesSlider.value < fastestFee * 1.2) {
document.getElementById('fee_rate_speed_text').innerText = 'Very fast (10 minutes)';
document.getElementById('fee_rate_speed_text').innerText = '{{ _("Very fast (10 minutes)") }}';
} else if (feesSlider.value >= fastestFee * 1.2) {
document.getElementById('fee_rate_speed_text').innerText = 'Overpaid! (10 minutes)';
document.getElementById('fee_rate_speed_text').innerText = '{{ _("Overpaid! (10 minutes)") }}';
}
}

View file

@ -26,8 +26,8 @@
<form action="{{ url_for('wallets_endpoint.send_new',wallet_alias=wallet_alias) }}" id="send-form" method="POST" style="width: 100%;">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="rbf_tx_id" value="{{ rbf_tx_id }}"/>
<h1 class="padded">Create Transaction</h1>
<p class="center">Available Funds: {{wallet.full_available_balance | btcunitamount}}
<h1 class="padded">{{ _("Create Transaction") }}</h1>
<p class="center">{{ _("Available Funds:") }} {{wallet.full_available_balance | btcunitamount}}
{% if specter.unit == 'sat' %}
sats
{% else %}
@ -38,7 +38,7 @@
{% endif %}
{% if specter.is_liquid and wallet.balance.get("assets", {}) %}
<span>
<br>Assets:<br>
<br>{{ _("Assets:") }}<br>
{% for asset in wallet.balance.get("assets",{}).keys() | sort %}
{% set balance = wallet.balance.get("assets",{}).get(asset, {}) %}
<span style="margin: 0 10px;">
@ -55,12 +55,12 @@
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Funds waiting in unsigned transactions<br><br></span>
To make funds available to spend in new transactions, just click on the "Unsigned" tab and delete unsigned transaction you have no need for.
<span class="info__title">{{ _("Funds waiting in unsigned transactions") }}<br><br></span>
{{ _('To make funds available to spend in new transactions, just click on the "Unsigned" tab and delete unsigned transaction you have no need for.') }}
</span>
</p>
</div>
<p style="display: inline; margin: auto; text-align: center;">&nbsp;You have waiting {{wallet.locked_amount | btcunitamount}} {% if specter.unit == 'sat' %}sats{% else %}{% if specter.is_testnet %}t{%endif%}BTC{% endif %} still in unsigned transactions.</p>
<p style="display: inline; margin: auto; text-align: center;">&nbsp;{{ _("You have waiting") }} {{wallet.locked_amount | btcunitamount}} {% if specter.unit == 'sat' %}sats{% else %}{% if specter.is_testnet %}t{%endif%}BTC{% endif %} {{ _("still in unsigned transactions.") }}</p>
<br>
<br>
</div>
@ -68,24 +68,24 @@
<div class="card" style="margin: auto;">
<div id="recipients" {% if ui_option != 'ui' %}class="hidden"{% endif %}></div>
<div id="recipients-txt-container" {% if ui_option == 'ui' %}class="hidden"{% endif %}>
Unit:
{{ _("Unit:") }}
<label><input type="radio" class="inline" style="margin: 0 5px;" name="amount_unit_text" value="sat" onchange="toggleUnit(this, 'text')" {% if specter.unit == 'sat' %}checked{% endif %}>sat</label>
<label><input type="radio" class="inline" style="margin: 0 5px;" name="amount_unit_text" value="btc" onchange="toggleUnit(this, 'text')" {% if specter.unit != 'sat' %}checked{% endif %}>BTC</label><br>
<textarea id="recipients-txt" name="recipients" placeholder="Enter recipient address, amount" style="margin-top: 5px; font-size: 0.95em;" onblur="validateForm()">{{ recipients_txt }}</textarea>
<textarea id="recipients-txt" name="recipients" placeholder="{{ _('Enter recipient address, amount') }}" style="margin-top: 5px; font-size: 0.95em;" onblur="validateForm()">{{ recipients_txt }}</textarea>
<p class="note" style="line-height: 2; background-color: #16202d; padding: 10px; border-radius: 7px;">
Enter each recipient address, amount as a new line:<br>
&lt;ADDRESS1&gt;, &lt;AMOUNT1&gt;<br>
&lt;ADDRESS2&gt;, &lt;AMOUNT2&gt;<br>
&lt;ADDRESS3&gt;, &lt;AMOUNT3&gt;<br>
{{ _("Enter each recipient address, amount as a new line:") }}<br>
&lt;{{ _("ADDRESS") }}1&gt;, &lt;{{ _("AMOUNT") }}1&gt;<br>
&lt;{{ _("ADDRESS") }}2&gt;, &lt;{{ _("AMOUNT") }}2&gt;<br>
&lt;{{ _("ADDRESS") }}3&gt;, &lt;{{ _("AMOUNT") }}3&gt;<br>
</p>
</div>
<button id="remove-recipient" style="width: 200px; height: 38px;" type="button" class="btn hidden" onclick="removeRecipient()"><b>&#8722;</b>&nbsp; &nbsp;Remove recipient</button>
<p><span id="calculated_tx_fee_label" class="note" style="margin-top: 17px;" onclick="calculateEstimatedFee()">Calculate estimated fee</span><span class="note" id="calculated_tx_fee"></span><p>
<span id="toggle_advanced" style="cursor: pointer;">Advanced {% if show_advanced_settings %}&#9660;{% else %}&#9654;{% endif %}</span>
<button id="remove-recipient" style="width: 200px; height: 38px;" type="button" class="btn hidden" onclick="removeRecipient()"><b>&#8722;</b>&nbsp; &nbsp;{{ _("Remove recipient") }}</button>
<p><span id="calculated_tx_fee_label" class="note" style="margin-top: 17px;" onclick="calculateEstimatedFee()">{{ _("Calculate estimated fee") }}</span><span class="note" id="calculated_tx_fee"></span><p>
<span id="toggle_advanced" style="cursor: pointer;">{{ _("Advanced") }} {% if show_advanced_settings %}&#9660;{% else %}&#9654;{% endif %}</span>
<br><br>
<div id="advanced_settings" style="margin: auto; max-width: 90%; display: {% if show_advanced_settings %}block{% else %}none{% endif %};">
<div>Transaction editor:
<div>{{ _("Transaction editor:") }}
<label><input type="radio" class="inline" style="margin: 0 10px 0 20px;" name="ui_option" value="ui" onclick="toggleSendUIType(this)" {% if ui_option == 'ui' %}checked{% endif %} id="ui-radio-btn">UI</label>
<label><input type="radio" class="inline" style="margin: 0 10px 0 20px;" name="ui_option" value="text" onclick="toggleSendUIType(this);" {% if ui_option != 'ui' %}checked{% endif %}>text</label>
</div><br>
@ -95,17 +95,17 @@
<br>
<br>
<div class="row break-row-mobile" id="coin-selection-row" style="margin-top: 30px;">
<button id="add-recipient" style="width: 200px; height: 38px;" type="button" class="btn" onclick="addRecipient('', 0, 'btc', '')"><svg width="20" height="20" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg> Add recipient </button>
<button id="add-recipient" style="width: 200px; height: 38px;" type="button" class="btn" onclick="addRecipient('', 0, 'btc', '')"><svg width="20" height="20" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg> {{ _("Add recipient") }} </button>
{% if wallet.utxo | length > 0 %}
<button style="width: 200px; height: 38px; margin-left: 10px;" id="coinselect" type="button" class="btn" onclick="toggleExpand()">Select coins<span class="optional">&nbsp;manually</span></button>
<button style="width: 200px; height: 38px; margin-left: 10px;" id="coinselect" type="button" class="btn" onclick="toggleExpand()">{{ _("Select coins") }}<span class="optional">&nbsp;{{ _("manually") }}</span></button>
{% endif %}
</div><br>
{% with selected_coins=selected_coins %}
{% include "wallet/send/new/components/coin_selection.jinja" %}
{% endwith %}
</div>
<button onclick="validateForm(true)" type="button" name="action" value="createpsbt" id="create_psbt_btn" class="btn centered" style="margin-top: 20px;">Create <span class="optional">&nbsp;unsigned&nbsp;</span>transaction</button>
</div>
<button onclick="validateForm(true)" type="button" name="action" value="createpsbt" id="create_psbt_btn" class="btn centered" style="margin-top: 20px;">{{ _('Create <span class="optional">&nbsp;unsigned&nbsp;</span>transaction') }}</button>
</div>
</form>
{% endblock %}
@ -170,24 +170,24 @@
{% endif %}
let recipientForm = `
<div id="recipient_${i}" style="border-bottom: 1px solid var(--cmap-border); padding-bottom: 10px; margin-bottom: 10px;">
Recipient address:<br>
{{ _("Recipient address:") }}<br>
<div class="row">
<input type="text" id="address_${i}" name="address_${i}" oninput="validateForm()" value="${addr}"> &nbsp;
<qr-scanner id="address-scan-${i}" style="margin-top: 3px;">
<a slot="button" class="btn" style="height: 35px;">
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 26px; margin: 0px;" class="svg-white"> Scan</a>
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 26px; margin: 0px;" class="svg-white"> {{ _("Scan") }}</a>
</qr-scanner>
</div>
<br>
Address label (optional):<br>
{{ _("Address label (optional):") }}<br>
<input type="text" id="label_${i}" name="label_${i}" value="${label}">
<br><br>
Amount:<br>
{{ _("Amount:") }}<br>
<input style="width: 200px" type="number" name="amount_${i}" oninput="calculateConvertedUnit(${i})" id="amount_${i}" min=0 step="${step}" autocomplete="off" value="${amount}">
<input type="hidden" name="btc_amount_${i}" id="btc_amount_${i}">
<div class="mobile-only" style="margin-top: 10px;"></div>
${assetSelector}
<span class="note max-btn" style="margin-left: 5px;" id="send_max_${i}" onclick="setMaxAmount(${i})">(send max)</span>
<span class="note max-btn" style="margin-left: 5px;" id="send_max_${i}" onclick="setMaxAmount(${i})">({{ _("send max") }})</span>
<div>
<span class="note" id="converted_unit_amount_${i}">-</span> <span class="note" id="converted_unit_label_${i}">${convertedUnit}</span> <span class="note" id="converted_unit_alt_${i}"></span>
</div>
@ -334,7 +334,7 @@
async function setMaxAmount(i) {
let amountInput = document.getElementById('amount_' + i);
if (!validateAddress(i)) {
showError('Please first enter a valid address');
showError('{{ _("Please first enter a valid address") }}');
return;
}
if(units[i] == 'sat' || units[i] == 'btc'){
@ -382,7 +382,7 @@
if (isNaN(amount)) {
setVisibility('amount_errors_container', 'block');
document.getElementById('amount_errors_container')
.innerHTML = "Amount entered is invalid!";
.innerHTML = '{{ _("Amount entered is invalid!") }}';
return false;
}
@ -396,17 +396,17 @@
if (isAboveWalletBalance(unit, amount)) {
setVisibility('amount_errors_container', 'block');
document.getElementById('amount_errors_container')
.innerHTML = `You cannot send more than ${assetBalances[unit].balance} ${assetBalances[unit].label}!`;
.innerHTML = `{{ _("You cannot send more than") }} ${assetBalances[unit].balance} ${assetBalances[unit].label}!`;
return false;
} else if (shouldSelectMoreCoins(unit, amount)) {
setVisibility('amount_errors_container', 'block');
document.getElementById('amount_errors_container')
.innerHTML = "You need to select more coins to match your amount!";
.innerHTML = '{{ _("You need to select more coins to match your amount!") }}';
return false;
} else if (isInvalidValue(unit, amount)) {
setVisibility('amount_errors_container', 'block');
document.getElementById('amount_errors_container')
.innerHTML = "Invalid amount! Wrong unit?";
.innerHTML = '{{ _("Invalid amount! Wrong unit?") }}';
return false;
} else {
setVisibility('amount_errors_container', 'none');
@ -442,7 +442,7 @@
}
}
if (submitted && !amount) {
showError('Please enter the amount that you wish to send', 5000);
showError('{{ _("Please enter the amount that you wish to send") }}', 5000);
}
if (document.getElementById("amount_" + i).value == '') {
amount = 0;
@ -456,7 +456,7 @@
for(let i in amounts) {
if (!validateAddress(i)) {
if (submitted) {
showError('Please enter a valid recipient address', 5000);
showError('{{ _("Please enter a valid recipient address") }}', 5000);
}
createPSBTButton.setAttribute('type', 'button');
return;
@ -499,13 +499,13 @@
let advancedSettings = document.getElementById('advanced_settings');
if (advancedSettings.style.display === 'block') {
advancedSettings.style.display = 'none';
advancedButton.innerHTML = 'Advanced &#9654;';
advancedButton.innerHTML = '{{ _("Advanced") }} &#9654;';
if (isCoinSelectionActive()) {
toggleExpand();
}
} else {
advancedSettings.style.display = 'block';
advancedButton.innerHTML = 'Advanced &#9660;';
advancedButton.innerHTML = '{{ _("Advanced") }} &#9660;';
}
}
@ -582,7 +582,7 @@
document.getElementById('calculated_tx_fee').innerText = `: ${result.error}`;
}
} catch {
document.getElementById('calculated_tx_fee').innerText = `: failed to calculate transaction fees`;
document.getElementById('calculated_tx_fee').innerText = `: {{ _("failed to calculate transaction fees") }}`;
}
return -1;
}

View file

@ -14,7 +14,7 @@
{% if pending_psbt['address']|length == 1 %}
<address-label data-address="{{ pending_psbt['address'][0] }}" data-label="{{ wallet.getlabel(pending_psbt['address'][0]) }}" data-wallet="{{ wallet_alias }}"></address-label>
{% else %}
{{ pending_psbt['address']|length }} Recipients
{{ pending_psbt['address']|length }} {{ _("Recipients") }}
{% endif %}
</td>
<td>
@ -31,13 +31,13 @@
<form action="{{ url_for('wallets_endpoint.send_pending',wallet_alias=wallet_alias) }}" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="pending_psbt" value='{{ pending_psbt|tojson|safe }}'>
<button type="submit" name="action" value="openpsbt" class="btn" style="margin-right: 5px;">Open</button>
<button type="submit" name="action" value="openpsbt" class="btn" style="margin-right: 5px;">{{ _("Open") }}</button>
</form>
<form action="{{ url_for('wallets_endpoint.send_pending',wallet_alias=wallet_alias) }}" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="pending_psbt" value='{{ pending_psbt|tojson|safe }}'>
<button type="submit" name="action" value="deletepsbt" class="btn danger" style="margin-left: 5px;" onclick="javascript: form.action='./?pending=True';">Delete</button>
<button type="submit" name="action" value="deletepsbt" class="btn danger" style="margin-left: 5px;" onclick="javascript: form.action='./?pending=True';">{{ _("Delete") }}</button>
</form>
</div>
</td>

View file

@ -12,7 +12,7 @@
<table>
<thead>
<tr>
<th></th><th class="optional">Address</th><th>Amount</th><th class="optional">Time</th><th>Sigs</th><th>Actions</th>
<th></th><th class="optional">{{ _("Address") }}</th><th>{{ _("Amount") }}</th><th class="optional">{{ _("Time") }}</th><th>{{ _("Sigs") }}</th><th>{{ _("Actions") }}</th>
</tr>
</thead>
<tbody>
@ -28,7 +28,7 @@
}}
{% endfor %}
{% else %}
<td></td><td>No transactions yet.</td><td></td><td class="optional"></td><td class="optional"></td><td></td>
<td></td><td>{{ _("No transactions yet.") }}</td><td></td><td class="optional"></td><td class="optional"></td><td></td>
{% endif %}
</tbody>
</table>

View file

@ -3,10 +3,10 @@
{% block content %}
{% from 'wallet/send/components/send_nav.jinja' import send_nav %}
{{ send_nav('send_pending', wallet_alias) }}
<h1 class="padded">Transactions Awaiting Signing:</h1>
<h1 class="padded">{{ _("Transactions Awaiting Signing:") }}</h1>
<div class="table-holder">
{% from 'wallet/send/pending/components/pending_psbt_table.jinja' import pending_psbt_table %}
{{ pending_psbt_table(pending_psbts, wallet_alias, wallet, specter) }}
</div><br>
<span class="center note padded">Here you can manage PSBTs (Partially Signed Bitcoin Transactions) you have created but did not finish signing or broadcasted to the network.</span>
<span class="center note padded">{{ _("Here you can manage PSBTs (Partially Signed Bitcoin Transactions) you have created but did not finish signing or broadcasted to the network.") }}</span>
{% endblock %}

View file

@ -77,9 +77,9 @@
{% for i in range(psbt['address']|length) %}
<div style="word-break: break-all;">
{% if "asset" in psbt %}
Sending <b>{{ psbt["amount"][i] | btcamount }}</b> <asset-label data-asset="{{psbt['asset'][i]}}" data-label="{{psbt['asset'][i] | assetlabel}}"></asset-label>
{{ _("Sending") }} <b>{{ psbt["amount"][i] | btcamount }}</b> <asset-label data-asset="{{psbt['asset'][i]}}" data-label="{{psbt['asset'][i] | assetlabel}}"></asset-label>
{% else %}
Sending <b>{{ psbt["amount"][i] | btcunitamount }}</b>
{{ _("Sending") }} <b>{{ psbt["amount"][i] | btcunitamount }}</b>
{% if specter.unit == 'sat' %}
sats
{% else %}
@ -89,7 +89,7 @@
<span class="note">({{ psbt["amount"][i] | altunit }})</span>
{% endif %}
{% endif %}
to<b>
{{ _("to") }}<b>
{% set addr_label = wallet.getlabel(psbt['address'][i]) %}
<address-label data-address="{{ psbt['address'][i] }}" data-label="{{ addr_label }}" data-wallet="{{ wallet_alias }}"></address-label>
</b>
@ -102,32 +102,32 @@
{% endfor %}
{% if wallet.is_multisig %}
<div class="log" style="margin: auto;">
Acquired <b id="sigscount">{{ psbt['sigs_count'] }}</b> of <b>{{ wallet.sigs_required }}</b> signatures.
{{ _("Acquired") }} <b id="sigscount">{{ psbt['sigs_count'] }}</b> {{ _("of") }} <b>{{ wallet.sigs_required }}</b> {{ _("signatures") }}.
</div>
{% endif %}
<br>
<input type="checkbox" id="showdetails">
<label for="showdetails" class="btn togglebtn">
transaction details
{{ _("transaction details") }}
</label>
<div class="toggle" id="tx_details" style="word-break: break-all;">
<br>
<h2 class="tx_details_header">Transaction Info</h2>
<h2 class="tx_details_header">{{ _("Transaction Info") }}</h2>
<p style="text-align: left; background-color: #131a24; padding: 10px; line-height: 2; border-radius: 15px;">
<b>Total fee:</b> {{ psbt['fee'] | btc2sat }} sats {% if specter.price_check %}<span class="note">&nbsp;({{ psbt['fee'] | altunit }})</span>{% endif %}<br>
<b>Fee rate:</b>
<b>{{ _("Total fee:") }}/b> {{ psbt['fee'] | btc2sat }} sats {% if specter.price_check %}<span class="note">&nbsp;({{ psbt['fee'] | altunit }})</span>{% endif %}<br>
<b>{{ _("Fee rate:") }}</b>
{% if 'tx_full_size' in psbt %}
{{ (psbt['fee'] / psbt['tx_full_size']) | feerate }} sat/vbyte<br>
<b>Estimated size:</b> {{ psbt['tx_full_size'] }} vbytes<br>
<b>{{ _("Estimated size:") }}</b> {{ psbt['tx_full_size'] }} vbytes<br>
{% else %}
{{ (psbt['fee'] / psbt['tx']['weight']) | feerate }} sat/vbyte<br>
<b>Size:</b> {{ psbt['tx']['vsize'] }} vbytes<br>
<b>{{ _("Size:") }}</b> {{ psbt['tx']['vsize'] }} vbytes<br>
{% endif %}
<b>Inputs count:</b> {{ psbt['tx']['vin'] | length }}<br>
<b>Outputs count:</b> {{ psbt['tx']['vout'] | length }}
<b>{{ _("Inputs count:") }}</b> {{ psbt['tx']['vin'] | length }}<br>
<b>{{ _("Outputs count:") }}</b> {{ psbt['tx']['vout'] | length }}
</p>
<h2 class="tx_details_header">Inputs ({{psbt['tx']['vin'] | length}})</h2>
<h2 class="tx_details_header">{{ _("Inputs") }} ({{psbt['tx']['vin'] | length}})</h2>
{% for input in psbt['tx']['vin'] %}
{% set tx = wallet.gettransaction(input['txid'], decode=true) %}
{% set bg_color = '#131a24' %}
@ -137,17 +137,17 @@
{% endif %}
<p class="tx_info" style="text-align: left; background-color: {{ bg_color }};">
<b style="margin: auto;">Input #{{loop.index0}}</b><br><br>
<b>Transaction id:</b><br>
<b>{{ _("Transaction id:") }}</b><br>
{{ explorer_link('tx', input['txid'], input['txid'], specter.explorer) }}
(Output #{{ input['vout'] }})<br>
({{ _("Output") }} #{{ input['vout'] }})<br>
{% if tx %}
{% if address %}
{% set addr_label = wallet.getlabel(address) %}
<b>Address:</b>
<b>{{ _("Address:") }}</b>
{{ address }}
<br>
{% if addr_label != address %}
<b>Label:</b>
<b>{{ _("Label:") }}</b>
<address-label data-address="{{ address }}" data-label="{{ addr_label }}" data-wallet="{{ wallet_alias }}"></address-label>
<br>
{% endif %}
@ -155,58 +155,58 @@
{% if specter.is_liquid %}
{% set amount = psbt['inputs'][loop.index0].get('value', -1) %}
{% set asset = psbt['inputs'][loop.index0].get('asset', "") %}
<b>Amount:</b> {{ amount|btcamount }} <asset-label data-asset="{{asset}}" data-label="{{asset | assetlabel}}"></asset-label>
<b>{{ _("Amount:") }}</b> {{ amount|btcamount }} <asset-label data-asset="{{asset}}" data-label="{{asset | assetlabel}}"></asset-label>
{% else %}
{% set amount = psbt['inputs'][loop.index0].get('witness_utxo',{}).get('amount', -1) %}
<b>Amount:</b> {{ amount|btcamount }} BTC {% if specter.price_check %}<span class="note">&nbsp;({{ amount | altunit }})</span>{% endif %}
<b>{{ _("Amount:") }}</b> {{ amount|btcamount }} BTC {% if specter.price_check %}<span class="note">&nbsp;({{ amount | altunit }})</span>{% endif %}
{% endif %}
{% endif %}
</p>
{% endfor %}
<h2 class="tx_details_header">Outputs ({{psbt['tx']['vout']|length}})</h2>
<h2 class="tx_details_header">{{ _("Outputs") }} ({{psbt['tx']['vout']|length}})</h2>
{% for output in psbt['tx']['vout'] if output['scriptPubKey']['type'] != 'fee' %}
{% set address = output['scriptPubKey']['addresses'][0] if "addresses" in output["scriptPubKey"] else output["scriptPubKey"]["address"] %}
{% set bg_color = '#154984' if wallet.is_address_mine(address) else '#131a24' %}
<p class="tx_info" style="text-align: left; background-color: {{ bg_color }};">
<b>Output #{{loop.index0}}</b> {% if address not in psbt['address'] %}(Change){% endif %}<br><br>
<b>{{ _("Output") }} #{{loop.index0}}</b> {% if address not in psbt['address'] %}(Change){% endif %}<br><br>
{% set addr_label = wallet.getlabel(address) %}
<b>Address:</b>
<b>{{ _("Address:") }}</b>
{{ address }}
<br>
{% if addr_label != address %}
<b>Label:</b>
<b>{{ _("Label:") }}</b>
<address-label data-address="{{ address }}" data-label="{{ addr_label }}" data-wallet="{{ wallet_alias }}"></address-label>
<br>
{% endif %}
<b>Amount:</b> {{ output['value']|btcamount }} BTC {% if specter.price_check %}<span class="note">&nbsp;({{ output['value'] | altunit }})</span>{% endif %}
<b>{{ _("Amount:") }}</b> {{ output['value']|btcamount }} BTC {% if specter.price_check %}<span class="note">&nbsp;({{ output['value'] | altunit }})</span>{% endif %}
</p>
{% endfor %}
Raw PSBT:<textarea id="raw-psbt" disabled style="background-color: #131a24;">{{ psbt['base64'] }}</textarea>
{{ _("Raw PSBT:") }}<textarea id="raw-psbt" disabled style="background-color: #131a24;">{{ psbt['base64'] }}</textarea>
<div class="row break-row-mobile" style="margin:auto">
<a id="download-psbt-btn" class="btn"
download="binary_{{ psbt['tx']['hash'] }}.psbt" href="data:application/octet-stream;base64;content-disposition=attachment,{{ psbt['base64'] }}">
<img src="{{ url_for('static', filename='img/file.svg') }}" style="width: 26px; margin: 0px;" class="svg-white">
Save binary
{{ _("Save binary") }}
</a>&nbsp;
<a id="download-psbt-btn" class="btn"
download="base64_{{ psbt['tx']['hash'] }}.psbt" href="data:text/plain;content-disposition=attachment,{{ psbt['base64'] }}">
<img src="{{ url_for('static', filename='img/file.svg') }}" style="width: 26px; margin: 0px;" class="svg-white">
Save base64
{{ _("Save base64") }}
</a>&nbsp;
<a id="copy-raw-psbt-btn" class="btn">
<img src="{{ url_for('static', filename='img/copy.svg') }}" style="width: 26px; margin: 0px;" class="svg-white">
Copy Raw PSBT
{{ _("Copy Raw PSBT") }}
</a>&nbsp;
<a id="show-qr-psbt-btn" class="btn">
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 26px; margin: 0px;" class="svg-white">
Show QR code
{{ _("Show QR code") }}
</a>
</div>
</div>
</div>
<div id="signing_container" class="signing_container flex-column {% if psbt['raw'] %}hidden{% endif %}" style="text-align: center;">
<p style="margin-bottom: 15px; margin-top: 0px;">Sign transaction with your:</p>
<p style="margin-bottom: 15px; margin-top: 0px;">{{ _("Sign transaction with your:") }}</p>
{% for device in wallet.devices %}
<button type="button" class="btn signing-column-btn" id="{{ device.alias }}_tx_sign_btn" {{ 'disabled style=background-color:#303c49;' if device.alias in psbt["devices_signed"] else '' }}>
{{ device.name }} {% if device.alias in psbt['devices_signed'] %} (&#10004;) {% endif %}
@ -217,26 +217,26 @@
{# ===================== Possible tx signers' outputs ===================== #}
<div id="ready_container" class="output_option {% if not psbt['raw'] %}hidden{% endif %}">
<p>Transaction is ready to send (&#10004;)</p>
<p>{{ _("Transaction is ready to send") }} (&#10004;)</p>
<div class="output_option">
<a class="btn centered" id="send_tx_btn">Send transaction</a>
<a class="btn centered" id="send_tx_btn">{{ _("Send transaction") }}</a>
</div>
</div>
<div id="paste_signed" class="output_option {% if psbt['raw'] %}hidden{% endif %}" style="margin-top:20px">
<button type="button" onclick="showPageOverlay('paste-tx-popup')" class="btn centered" id="pastetx">Paste signed transaction</a>
<button type="button" onclick="showPageOverlay('paste-tx-popup')" class="btn centered" id="pastetx">{{ _("Paste signed transaction") }}</a>
</div>
<div class="hidden" id="paste-tx-popup">
<h1>Paste signed transaction</h1>
<input type="text" id="paste-tx-text" placeholder="Paste your transaction here">
<h1>{{ _("Paste signed transaction") }}</h1>
<input type="text" id="paste-tx-text" placeholder="{{ _('Paste your transaction here') }}">
<br><br>
<button type="button" onclick="currentSigningDevice = '';combine(document.getElementById('paste-tx-text').value);hidePageOverlay();document.getElementById('paste-tx-text').value='';" class="btn centered">Confirm</button>
<button type="button" onclick="currentSigningDevice = '';combine(document.getElementById('paste-tx-text').value);hidePageOverlay();document.getElementById('paste-tx-text').value='';" class="btn centered">{{ _("Confirm") }}</button>
</div>
{% include 'includes/overlay/broadcast_tx.jinja' %}
<div class="row padded">
<input type="hidden" name="pending_psbt" value='{{psbt|tojson|safe}}'>
<button id="deletepsbt_btn" type="submit" name="action" value="deletepsbt" class="btn danger centered" style="margin-left: 5px;">Delete Transaction</button>
<button id="deletepsbt_btn" type="submit" name="action" value="deletepsbt" class="btn danger centered" style="margin-left: 5px;">{{ _("Delete Transaction") }}</button>
</div>
</form>
@ -415,7 +415,7 @@
}
async function broadcastLocal(tx) {
showNotification("Sending transaction...");
showNotification("{{ _('Sending transaction...') }}");
let url="{{ url_for('wallets_endpoint.broadcast', wallet_alias=wallet.alias) }}";
var formData = new FormData();
@ -437,15 +437,15 @@
hidePageOverlay();
window.location.replace("{{ url_for('wallets_endpoint.history', wallet_alias=wallet.alias) }}");
} else {
showError("Server failed to broadcast transactions!\n" + jsonResponse.error);
showError("{{ _('Server failed to broadcast transactions!') }}\n" + jsonResponse.error);
}
} catch(e) {
showError("Server failed to broadcast transactions!\n" + e);
showError("{{ _('Server failed to broadcast transactions!') }}\n" + e);
}
}
async function broadcastBlockExplorer(tx, explorer, useTor) {
showNotification("Sending transaction...", 0);
showNotification("{{ _('Sending transaction...') }}", 0);
let url = "{{ url_for('wallets_endpoint.broadcast_blockexplorer', wallet_alias=wallet.alias) }}";
@ -470,10 +470,10 @@
hidePageOverlay();
window.location.replace("{{ url_for('wallets_endpoint.history', wallet_alias=wallet.alias) }}");
} else {
showError("Server failed to broadcast transactions!\n" + jsonResponse.error);
showError("{{ _('Server failed to broadcast transactions!') }}\n" + jsonResponse.error);
}
} catch(e) {
showError("Server failed to broadcast transactions!\n" + e);
showError("{{ _('Server failed to broadcast transactions!') }}\n" + e);
}
}

View file

@ -13,20 +13,20 @@
</style>
<br>
<nav class="row collapse-on-mobile">
<button type="button" id="info_settings_tab_btn" class="btn radio left checked" onclick="setSettingsView('info')"> Wallet Info </button>
<button type="button" id="export_settings_tab_btn" class="btn radio" onclick="setSettingsView('export')"> Export </button>
<button type="button" id="advanced_settings_tab_btn" class="btn radio right" onclick="setSettingsView('advanced')"> Advanced </button>
<button type="button" id="info_settings_tab_btn" class="btn radio left checked" onclick="setSettingsView('info')"> {{ _("Wallet Info") }} </button>
<button type="button" id="export_settings_tab_btn" class="btn radio" onclick="setSettingsView('export')"> {{ _("Export") }} </button>
<button type="button" id="advanced_settings_tab_btn" class="btn radio right" onclick="setSettingsView('advanced')"> {{ _("Advanced") }} </button>
<a href="javascript:void(0);" class="mobile-nav-icon" onclick="toggleMobileNav(this, `{{ url_for('static', filename='img/caret-down.svg') }}`, `{{ url_for('static', filename='img/caret-up.svg') }}`)">
<img style="width: 36px;" src="{{ url_for('static', filename='img/caret-down.svg') }}" class="svg-white"/>
</a>
</nav><br>
<div id="wallet_info_settings_tab" style="width: 100%">
<h1 style="font-size: 1.7em;">Wallet Info</h1><br>
<h1 style="font-size: 1.7em;">{{ _("Wallet Info") }}</h1><br>
{% if wallet.is_multisig %}
<h2 class="subtitle">Devices</h2>
<h2 class="subtitle">{{ _("Devices") }}</h2>
<p style="text-align: center; font-size: 1.2em;">{{ wallet.sigs_required }} out of {{ wallet.keys|length }} multisig</p>
{% else %}
<h2 class="subtitle">Device</h2>
<h2 class="subtitle">{{ _("Device") }}</h2>
{% endif %}
<div class="row overflow">
{% for device in wallet.devices %}
@ -38,28 +38,28 @@
</a>
{% endfor %}
</div>
<button onclick="toggleKeysList()" id="toggle_keys_list" type="button" class="btn centered padded">Show keys details</button>
<button onclick="toggleKeysList()" id="toggle_keys_list" type="button" class="btn centered padded">{{ _("Show keys details") }}</button>
<br>
<div id="keys_list" class="center" style="margin: auto; display: none; width: 85%;">
<label style="font-size: 1.3em;">Use SLIP-132:&nbsp;</label>
<label style="font-size: 1.3em;">{{ _("Use SLIP-132:") }}&nbsp;</label>
<label class="switch">
<input type="checkbox" id="use_slip_132" onchange="toggleKeyDisplay()">
<span class="slider"></span>
</label><br>
<p class="note" style="margin-top: 5px; max-width: 90%;">SLIP-132 extended public keys (xpubs) are a different format for xpubs which usually starts with zpub or ypub.</p>
<p class="note" style="margin-top: 5px; max-width: 90%;">{{ _("SLIP-132 extended public keys (xpubs) are a different format for xpubs which usually starts with zpub or ypub.") }}</p>
<br>
{% for i in range(wallet.devices | length) %}
{% set device = wallet.devices[i] %}
{% set key = wallet.keys[i] %}
<h1>Signer {% if wallet.is_multisig %}{{ loop.index }} {% endif %}- {{ device.name }}</h1>
<h1>{{ _("Signer") }} {% if wallet.is_multisig %}{{ loop.index }} {% endif %}- {{ device.name }}</h1>
<div class="table-holder">
<table>
<thead>
<tr>
<th>Network</th>
<th>Purpose</th>
<th class="optional">Derivation</th>
<th class="optional">Key</th>
<th>{{ _("Network") }}</th>
<th>{{ _("Purpose") }}</th>
<th class="optional">{{ _("Derivation") }}</th>
<th class="optional">{{ _("Key") }}</th>
</tr>
</thead>
<tbody>
@ -79,7 +79,7 @@
</div>
</div>
<div class="hidden" id="wallet_export_settings_tab">
<h1 style="font-size: 1.7em;">Export Wallet</h1>
<h1 style="font-size: 1.7em;">{{ _("Export Wallet") }}</h1>
<div class="card center">
{% set supports_export_to_device = [] %}
{% for device in wallet.devices if device.exportable_to_wallet %}
@ -89,9 +89,9 @@
{% endif %}
{% endfor %}
{% if supports_export_to_device != [] %}
<h2 class="subtitle">Export To Device</h1>
<h2 class="subtitle">{{ _("Export To Device") }}</h1>
<p class="center">
Import this wallet to a device by scanning its QR code or importing its data file.
{{ _("Import this wallet to a device by scanning its QR code or importing its data file.") }}
</p>
{% for device in wallet.devices %}
@ -101,7 +101,7 @@
download="{{ wallet.name | ascii20 }}.txt"
href="data:text/plain;charset=US-ASCII,{{ device.export_wallet(wallet) }}"
{% if device.device_type == 'coldcard' %}
onclick="showNotification('Import wallet file to your ColdCard: Settings -> Multisig Wallets -> Import from SD', 0)"
onclick="showNotification(`{{ _('Import wallet file to your ColdCard: Settings -> Multisig Wallets -> Import from SD') }}`, 0)"
{% endif %}
class="btn centered padded">
Save {{ device.name }} file
@ -109,7 +109,7 @@
{% elif device.wallet_export_type == 'qr' %}
<button onclick="showPageOverlay('{{ device.alias }}_export_qr_code')" type="button" class="btn centered padded">Show {{ device.name }} QR Code</button>
<div id="{{ device.alias }}_export_qr_code" style="display: none;">
<h1>Scan this with your {{ device.name }} device </h1>
<h1>{{ _("Scan this with your") }} {{ device.name }} {{ _("device") }} </h1>
<qr-code value="{{ device.export_wallet(wallet) }}" width="400" scalable></qr-code>
</div>
{% endif %}
@ -118,61 +118,61 @@
<div class="section-separator"></div>
{% endif %}
<h2 class="subtitle">Export To Wallet Software</h2>
<h2 class="subtitle">{{ _("Export To Wallet Software") }}</h2>
<p class="center">
Import this wallet to another Specter instance or other supported wallet softwares by scanning the QR code or importing the JSON file.
{{ _("Import 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="btn centered padded">Export</button>
<button onclick="showPageOverlay('account_map_export')" type="button" class="btn centered padded">{{ _("Export") }}</button>
<div class="section-separator"></div>
<h2 class="subtitle">Export PDF backup</h2>
<h2 class="subtitle">{{ _("Export PDF backup") }}</h2>
<p style="max-width: 500px;" class="center">
It is recommended that you print and save this wallet backup file with each of your devices.
{{ _("It is recommended that you print and save this wallet backup file with each of your devices.") }}
</p>
<button id="pdf-wallet-download" type="button" class="btn centered padded" style="margin: 30px auto;">Save Backup PDF</button>
<span id="advanced_pdf_export_toggle" style="cursor: pointer;" onclick="togglePDFExportAdvanced()">Advanced &#9654;</span>
<span id="advanced_pdf_export_toggle" style="cursor: pointer;" onclick="togglePDFExportAdvanced()">{{ _("Advanced") }} &#9654;</span>
<br>
<div style="margin: 0 auto;" class="center hidden" id="advanced_pdf_export_container"><br>
<label style="font-size: 1em;"">Use SLIP-132:&nbsp;</label>
<label style="font-size: 1em;">{{ _("Use SLIP-132:") }}&nbsp;</label>
<label class="switch">
<input type="checkbox" id="use_slip_132_pdf" style="margin: auto;" onchange="toggleKeyDisplay()">
<span class="slider"></span>
</label><br>
</div>
<br>
<p class="note center" style="margin-top: 5px; max-width: 90%;">To format master keys on the PDF backup document.</p>
<p class="note center" style="margin-top: 5px; max-width: 90%;">{{ _("To format master keys on the PDF backup document.") }}</p>
<p style="max-width: 500px;" class="note center">
This backup includes all the information you'd need to restore your wallet in Specter Desktop or other compatible wallets including Bitcoin Core itself.<br>
{{ _("This backup includes all the information you'd need to restore your wallet in Specter Desktop or other compatible wallets including Bitcoin Core itself.") }}<br>
<p>
<p style="max-width: 500px;" class="note center">
It is recommended that you keep this file private, as it contains all the information needed to track all of the wallet balance and transactions history.
{{ _("It is recommended that you keep this file private, as it contains all the information needed to track all of the wallet balance and transactions history.") }}
<p>
{% include 'wallet/components/wallet_pdf.jinja' %}
{% include "wallet/settings/components/export_wallet_account_map.jinja" %}
</div>
</div>
<div class="hidden" id="wallet_advanced_settings_tab">
<h1 style="font-size: 1.7em;">Advanced Options</h1>
<h1 style="font-size: 1.7em;">{{ _("Advanced Options") }}</h1>
<div class="card">
<h2 class="subtitle">Keypool control</h2>
<h2 class="subtitle">{{ _("Keypool control") }}</h2>
<form action="." method="POST" class="padded" style="max-width: 500px">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<div>Currently watching {{ wallet.keypool }} receiving and {{ wallet.change_keypool }} change addresses.</div>
<div>{{ _("Currently watching") }} {{ wallet.keypool }} {{ _("receiving and") }} {{ wallet.change_keypool }} {{ _("change addresses.") }}</div>
<div class="row aligned">
<div style="flex-grow: 1">Watch <input type="number" name="keypooladd" value="100" step="1" style="margin: 10px" class="inline"> more addresses </div>
<button type="submit" name="action" value="keypoolrefill" class="btn" style="max-width: 130px; margin-left: 20px">Add</button>
<div style="flex-grow: 1">{{ _("Watch") }} <input type="number" name="keypooladd" value="100" step="1" style="margin: 10px" class="inline"> {{ _("more addresses") }} </div>
<button type="submit" name="action" value="keypoolrefill" class="btn" style="max-width: 130px; margin-left: 20px">{{ _("Add") }}</button>
</div>
</form>
<div class="section-separator"></div>
<h2 class="subtitle">Blockchain rescan</h2>
<h2 class="subtitle">{{ _("Blockchain rescan") }}</h2>
<form action="." method="POST" class="padded" style="max-width: 500px">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
{% if wallet.rescan_progress %}
<div class="row aligned">
<div style="margin-right: 30px; flex-grow: 1;">Rescanning blockchain: <span id="{{ wallet.alias }}_wallet_rescan_percents">{{"%.2f" % (wallet.rescan_progress*100)}}</span>%</div>
<button type="submit" name="action" value="abortrescan" class="btn" style="max-width: 130px; margin-left: 20px">Abort rescan</button>
<button type="submit" name="action" value="abortrescan" class="btn" style="max-width: 130px; margin-left: 20px">{{ _("Abort rescan") }}</button>
</div>
{% else %}
Rescan blockchain from block:<br>
{{ _("Rescan blockchain from block:") }}<br>
<div class="row aligned">
{% if specter.info.chain == "main" %}
{% set startblock=(481824 if not specter.info['pruned'] or specter.info['pruneheight'] < 481824 else specter.info['pruneheight']) %}
@ -183,31 +183,31 @@
<button type="submit" name="action" value="rescanblockchain" class="btn">Rescan</button>
</div>
{% if specter.info['pruned'] %}
<div class="note center full-width">Note: You are using a pruned node. Rescan is limited by the pruned height to block: {{specter.info['pruneheight']}}</div>
<div class="note center full-width">{{ _("Note: You are using a pruned node. Rescan is limited by the pruned height to block:") }} {{specter.info['pruneheight']}}</div>
{% endif %}
<div class="note center"><b>481824</b> was the first Segwit block.</div>
<div class="note center"><b>481824</b> {{ _("was the first Segwit block.") }}</div>
{% endif %}
</form>
<form action="." method="POST" class="padded" style="max-width: 500px">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
{% if specter.info["utxorescan"] %}
<div class="row aligned padded">
<div style="flex-grow: 1">UTXO rescan in process: <span id="wallet_rescan_percents">{{ "%.2f"|format(specter.info["utxorescan"]) }}</span>%.</div>
<button type="submit" name="action" value="abortrescanutxo" class="btn" style="max-width: 130px; margin-left: 20px">Abort rescan</button>
<div style="flex-grow: 1">{{ _("UTXO rescan in process:") }} <span id="wallet_rescan_percents">{{ "%.2f"|format(specter.info["utxorescan"]) }}</span>%.</div>
<button type="submit" name="action" value="abortrescanutxo" class="btn" style="max-width: 130px; margin-left: 20px">{{ _("Abort rescan") }}</button>
</div>
{% else %}
<div class="row aligned">
<div style="flex-grow: 1">Or rescan only unspent transactions:
<div style="flex-grow: 1">{{ _("Or rescan only unspent transactions:") }}
</div>
<button type="submit" name="action" value="rescanutxo" class="btn" style="max-width: 180px; margin-left: 20px">
Rescan UTXO
{{ _("Rescan UTXO") }}
<div class="tool-tip">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Rescanning UTXO set</span><br><br>
Only unspent transactions will be imported, so you won't see the full history, but your balance will be there.<br><br>
<b>Note:</b> Make sure you have enough addresses in the keypool, otherwise not all unspent transactions may be found.
<span class="info__title">{{ _("Rescanning UTXO set") }}</span><br><br>
{{ _("Only unspent transactions will be imported, so you won't see the full history, but your balance will be there.") }}<br><br>
<b>{{ _("Note:") }}</b> {{ _("Make sure you have enough addresses in the keypool, otherwise not all unspent transactions may be found.") }}
</span>
</p>
</div>
@ -215,22 +215,22 @@
</div>
{% if specter.info['pruned'] %}
<div class="row" style="margin-top: 10px">
<label for="use_explorer"><input style="width: auto; min-width: auto;" type="checkbox" id="use_explorer" name="use_explorer" checked> fetch missing data from block explorer
<label for="use_explorer"><input style="width: auto; min-width: auto;" type="checkbox" id="use_explorer" name="use_explorer" checked> {{ _("fetch missing data from block explorer") }}
<tool-tip title="Fetch missing data">
You are using a pruned node. Blocks for some transactions may be missing.<br><br>
Check this checkbox if you want to fetch missing information from block explorer.
{{ _("You are using a pruned node. Blocks for some transactions may be missing.") }}<br><br>
{{ _("Check this checkbox if you want to fetch missing information from block explorer.") }}
</tool-tip>
</label>
</div>
<div id="utxo-explorer">
<br>
Block explorer URL:
{{ _("Block explorer URL:") }}
{% include "components/explorer_select.jinja" %}
<span class="warning" id="utxo-warning" style="display: inline-block; max-width:300px;">
<img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>Careful with block explorers!<br>
This may have privacy implication!<br>
Information fetched from the block explorer contains a list of unspent transactions of your wallet from old (pruned) blocks.<br>
"They" will know what transactions you are interested in.
<img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>{{ _("Careful with block explorers!") }}<br>
{{ _("This may have privacy implications!") }}<br>
{{ _("Information fetched from the block explorer contains a list of unspent transactions of your wallet from old (pruned) blocks.") }}<br>
{{ _('"They" will know what transactions you are interested in.') }}
</span>
</div>
<script type="text/javascript">
@ -249,10 +249,10 @@
{% endif %}
</form>
<div class="section-separator"></div>
<h2 class="subtitle">Delete wallet</h2>
<h2 class="subtitle">{{ _("Delete wallet") }}</h2>
<form action="." method="POST" class="padded" style="max-width: 500px">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" id="delete_wallet" name="action" value="deletewallet" class="btn danger centered" style="max-width: 160px;">Delete Wallet</button>
<button type="submit" id="delete_wallet" name="action" value="deletewallet" class="btn danger centered" style="max-width: 160px;">{{ _("Delete Wallet") }}</button>
</form>
</div>
</div>
@ -266,10 +266,10 @@
let keysList = document.getElementById('keys_list');
if (keysList.style.display !== 'none') {
keysList.style.display = 'none';
titleButton.innerHTML = 'Show keys details';
titleButton.innerHTML = '{{ _("Show keys details") }}';
} else {
keysList.style.display = 'block';
titleButton.innerHTML = 'Hide keys details';
titleButton.innerHTML = '{{ _("Hide keys details") }}';
}
}
@ -299,10 +299,10 @@
let advancedSettings = document.getElementById(`advanced_pdf_export_container`);
if (advancedSettings.classList.contains("hidden")) {
advancedSettings.classList.remove("hidden");
advancedButton.innerHTML = 'Advanced &#9660;';
advancedButton.innerHTML = '{{ _("Advanced") }} &#9660;';
} else {
advancedSettings.classList.add("hidden");
advancedButton.innerHTML = 'Advanced &#9654;';
advancedButton.innerHTML = '{{ _("Advanced") }} &#9654;';
}
}

Some files were not shown because too many files have changed in this diff Show more