diff --git a/.github/workflows/zblack.yml b/.github/workflows/zblack.yml index 56b86da551..4a54b98df9 100644 --- a/.github/workflows/zblack.yml +++ b/.github/workflows/zblack.yml @@ -8,4 +8,9 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - uses: psf/black@stable # https://black.readthedocs.io/en/stable/integrations/github_actions.html + # This version should match what you can find in .pre-commit-config.yaml + - uses: psf/black@22.3.0 # https://black.readthedocs.io/en/stable/integrations/github_actions.html + with: + options: "--check --verbose" + src: "./src" + version: "22.3.0" diff --git a/.vscode/launch.json b/.vscode/launch.json index ccb112863e..a633ef7dfd 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,7 +1,7 @@ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + // For more information, visit: https://code.visualstudio.com/docs/editor/debugging#_launch-configurations "version": "0.2.0", "configurations": [ { diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..e137fadb9e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/cypress/integration/spec_balances_amounts.js b/cypress/integration/spec_balances_amounts.js index 098001b52f..5a18cad35b 100644 --- a/cypress/integration/spec_balances_amounts.js +++ b/cypress/integration/spec_balances_amounts.js @@ -75,6 +75,7 @@ describe('Test the rendering of balances and amounts', () => { cy.get('#satoshis_hot_keys_hot_sign_btn').click() cy.get('#hot_enter_passphrase__submit').click() cy.get('#broadcast_local_btn').click() + cy.visit("/") cy.selectWallet('Ghost wallet') // Once again because only once doesn't work for some stupid unknown reason cy.selectWallet('Ghost wallet') diff --git a/cypress/integration/spec_configures_nodes.js b/cypress/integration/spec_configures_nodes.js index 958a9ce47b..ef792eea5f 100644 --- a/cypress/integration/spec_configures_nodes.js +++ b/cypress/integration/spec_configures_nodes.js @@ -5,13 +5,15 @@ describe('Configuring nodes', () => { cy.viewport(1200,660) cy.visit('/') cy.get('#node-switch-icon').click() - cy.get('[href="/nodes/node/default/"]').first().click() + cy.get('[data-cy="connect-new-node-btn"]').click() + cy.get(':nth-child(6) > [href="/nodes/new_node/"]').click() cy.get('#datadir-container').then(($datadir) => { cy.log($datadir) if (!Cypress.dom.isVisible($datadir)) { cy.get('.slider').click() } }) + cy.get('#name').type('Bitcoin Core') cy.get('.slider').click() cy.get('#username').clear() cy.get('#username').type("bitcoin") diff --git a/cypress/integration/spec_empty_specter_home.js b/cypress/integration/spec_empty_specter_home.js index 5ac14ed956..b2f1219bbb 100644 --- a/cypress/integration/spec_empty_specter_home.js +++ b/cypress/integration/spec_empty_specter_home.js @@ -8,9 +8,6 @@ describe('Completely empty specter-home', () => { cy.viewport(1200,660) cy.visit('/welcome/about') cy.contains('Welcome to Specter Desktop') - cy.get('#node-switch-icon').click() - cy.get('[href="/nodes/node/default/"]').first().click() - cy.contains('Bitcoin Core') cy.get('[href="/settings/"] > img').click() cy.contains('Backup and Restore') cy.get('[href="/settings/auth"]').click() diff --git a/package-lock.json b/package-lock.json index a5e24c5658..9dc1bb12d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -97,9 +97,9 @@ } }, "node_modules/@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" }, "node_modules/@sideway/pinpoint": { "version": "2.0.0", @@ -1928,9 +1928,9 @@ } }, "@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" }, "@sideway/pinpoint": { "version": "2.0.0", diff --git a/pyinstaller/electron/package-lock.json b/pyinstaller/electron/package-lock.json index 1bd79874e8..7eb6a3c0cf 100644 --- a/pyinstaller/electron/package-lock.json +++ b/pyinstaller/electron/package-lock.json @@ -1652,9 +1652,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, "node_modules/http-signature": { @@ -1870,9 +1870,9 @@ "dev": true }, "node_modules/json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -1906,17 +1906,17 @@ } }, "node_modules/jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "engines": [ - "node >=0.6.0" - ], + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", - "json-schema": "0.2.3", + "json-schema": "0.4.0", "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" } }, "node_modules/keyv": { @@ -4675,9 +4675,9 @@ } }, "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, "http-signature": { @@ -4841,9 +4841,9 @@ "dev": true }, "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" }, "json-schema-traverse": { "version": "0.4.1", @@ -4871,13 +4871,13 @@ } }, "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", - "json-schema": "0.2.3", + "json-schema": "0.4.0", "verror": "1.10.0" } }, diff --git a/requirements.in b/requirements.in index 11f8c00879..999a6fafc2 100644 --- a/requirements.in +++ b/requirements.in @@ -2,7 +2,7 @@ certifi==2022.12.7 chardet==3.0.4 Click==8.1.1 Flask==2.1.1 -Werkzeug==2.0.0 # need to pin because Flask doesn't work with newer ones +Werkzeug==2.0.3 # need to pin because Flask doesn't work with newer ones Flask-Babel==2.0.0 Flask-Cors==3.0.10 Flask-Login==0.5.0 @@ -25,6 +25,7 @@ mnemonic==0.20 cryptography==3.4.7 Flask-APScheduler==1.12.4 gunicorn==20.1.0 +simple-websocket==0.8.1 protobuf==3.20.2 PyJWT==2.4.0 pytimeparse==1.1.8 @@ -34,4 +35,4 @@ cryptoadvance-liquidissuer==0.2.4 specterext-exfund==0.1.7 specterext-faucet==0.1.2 cryptoadvance.spectrum==0.3.1 -specterext-stacktrack==0.2.1 \ No newline at end of file +specterext-stacktrack==0.2.1 diff --git a/requirements.txt b/requirements.txt index 7e0339e709..f2554eb74d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile +# This file is autogenerated by pip-compile with python 3.10 # To update, run: # # pip-compile --generate-hashes requirements.in @@ -12,9 +12,9 @@ apscheduler==3.9.1 \ --hash=sha256:65e6574b6395498d371d045f2a8a7e4f7d50c6ad21ef7313d15b1c7cf20df1e3 \ --hash=sha256:ddc25a0ddd899de44d7f451f4375fb971887e65af51e41e5dcf681f59b8b2c9a # via flask-apscheduler -babel==2.10.3 \ - --hash=sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51 \ - --hash=sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb +babel==2.11.0 \ + --hash=sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe \ + --hash=sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6 # via flask-babel base58==2.1.1 \ --hash=sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2 \ @@ -119,7 +119,7 @@ cryptoadvance-liquidissuer==0.2.4 \ --hash=sha256:5a2c531801854c5a4a46daf184877e22f731cdb42d2cfb840785bda7371ba6fb \ --hash=sha256:9e468f3e35ecc566b3f74a2263677cf26632548abb194521dba15ad37acd1e9b # via -r requirements.in -cryptoadvance.spectrum==0.3.1 \ +cryptoadvance-spectrum==0.3.1 \ --hash=sha256:c0eedce7d88f28a1fbf3a5afbcf3b03f1effdb0983f07445953a1d9fbe789209 \ --hash=sha256:c8eba63d3aec6cc391784e5dde99ec39b13f719a062ef1ce786e5fdf850150d8 # via -r requirements.in @@ -153,7 +153,21 @@ embit==0.6.1 \ --hash=sha256:16a84c6668dc9ffc907594457a46f7142cee379646bc009a5a9b77b0d2cb4e12 # via # -r requirements.in - # cryptoadvance.spectrum + # cryptoadvance-spectrum +flask==2.1.1 \ + --hash=sha256:8a4cf32d904cf5621db9f0c9fbcd7efabf3003f22a04e4d0ce790c7137ec5264 \ + --hash=sha256:a8c9bd3e558ec99646d177a9739c41df1ded0629480b4c8d2975412f3c9519c8 + # via + # -r requirements.in + # cryptoadvance-spectrum + # flask-apscheduler + # flask-babel + # flask-cors + # flask-httpauth + # flask-login + # flask-restful + # flask-sqlalchemy + # flask-wtf flask-apscheduler==1.12.4 \ --hash=sha256:681dae34dc6cc9403ce674795e53abd0bff540472129cfd3d3c93e0e1d502da8 # via -r requirements.in @@ -180,22 +194,8 @@ flask-restful==0.3.9 \ flask-sqlalchemy==2.5.1 \ --hash=sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912 \ --hash=sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390 - # via cryptoadvance.spectrum -flask==2.1.1 \ - --hash=sha256:8a4cf32d904cf5621db9f0c9fbcd7efabf3003f22a04e4d0ce790c7137ec5264 \ - --hash=sha256:a8c9bd3e558ec99646d177a9739c41df1ded0629480b4c8d2975412f3c9519c8 - # via - # -r requirements.in - # cryptoadvance.spectrum - # flask-apscheduler - # flask-babel - # flask-cors - # flask-httpauth - # flask-login - # flask-restful - # flask-sqlalchemy - # flask-wtf -flask_wtf==0.15.1 \ + # via cryptoadvance-spectrum +flask-wtf==0.15.1 \ --hash=sha256:6ff7af73458f182180906a37a783e290bdc8a3817fe4ad17227563137ca285bf \ --hash=sha256:ff177185f891302dc253437fe63081e7a46a4e99aca61dfe086fb23e54fff2dc # via -r requirements.in @@ -271,6 +271,10 @@ gunicorn==20.1.0 \ --hash=sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e \ --hash=sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8 # via -r requirements.in +h11==0.14.0 \ + --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ + --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 + # via wsproto hidapi==0.12.0.post2 \ --hash=sha256:00bad74617622b7b7abbb24eae5c11bd91957bb87217d62d8feea15c971a77b5 \ --hash=sha256:0292a29325b905fbfbb39cc368055cda63339fed684eceb7d4d241acc5e60d50 \ @@ -317,7 +321,7 @@ idna==3.4 \ --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 # via requests -importlib_metadata==4.8.1 \ +importlib-metadata==4.8.1 \ --hash=sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15 \ --hash=sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1 # via -r requirements.in @@ -594,7 +598,7 @@ psycopg2-binary==2.9.5 \ --hash=sha256:f95b8aca2703d6a30249f83f4fe6a9abf2e627aa892a5caaab2267d56be7ab69 # via # -r requirements.in - # cryptoadvance.spectrum + # cryptoadvance-spectrum pyaes==1.6.1 \ --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f # via hwi @@ -637,29 +641,33 @@ pytimeparse==1.1.8 \ --hash=sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd \ --hash=sha256:e86136477be924d7e670646a98561957e8ca7308d44841e21f5ddea757556a0a # via -r requirements.in -pytz-deprecation-shim==0.1.0.post0 \ - --hash=sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6 \ - --hash=sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d - # via tzlocal -pytz==2022.5 \ - --hash=sha256:335ab46900b1465e714b4fda4963d87363264eb662aab5e65da039c25f1f5b22 \ - --hash=sha256:c4d88f472f54d615e9cd582a5004d1e5f624854a6a27a6211591c251f22a6914 +pytz==2022.7.1 \ + --hash=sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0 \ + --hash=sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a # via # apscheduler # babel # flask-babel # flask-restful # pandas +pytz-deprecation-shim==0.1.0.post0 \ + --hash=sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6 \ + --hash=sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d + # via tzlocal requests==2.26.0 \ --hash=sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24 \ --hash=sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7 # via # -r requirements.in - # cryptoadvance.spectrum + # cryptoadvance-spectrum semver==2.13.0 \ --hash=sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4 \ --hash=sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f # via bitbox02 +simple-websocket==0.8.1 \ + --hash=sha256:0abe874f6a0c6ddd197dbd4f0ce5708b47610a0d14fa2b0762e515659573d44a \ + --hash=sha256:bab2f34151d8b9abb1ea6d911e5edc8232fc203b6f814d179db0ba3f93e4e026 + # via -r requirements.in six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 @@ -751,12 +759,16 @@ urllib3==1.26.12 \ --hash=sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e \ --hash=sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997 # via requests -werkzeug==2.0.0 \ - --hash=sha256:3389bbfe6d40c6dd25e6d3f974155163c8b3de5bbda6a89342d4ab93fae80ba0 \ - --hash=sha256:64c02f6495ba01eddd6625b3675f357cd358a73f1e38458a56ad86c5baa30b53 +werkzeug==2.0.3 \ + --hash=sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8 \ + --hash=sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c # via # -r requirements.in # flask +wsproto==1.2.0 \ + --hash=sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065 \ + --hash=sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736 + # via simple-websocket wtforms==3.0.1 \ --hash=sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc \ --hash=sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b diff --git a/src/cryptoadvance/specter/cli/cli_server.py b/src/cryptoadvance/specter/cli/cli_server.py index 9a49473995..aba1b2469c 100644 --- a/src/cryptoadvance/specter/cli/cli_server.py +++ b/src/cryptoadvance/specter/cli/cli_server.py @@ -125,6 +125,29 @@ def server( if key: app.config["KEY"] = key + # the app.config needs to be configured before init_app, such that the service callbacks + # like after_serverpy_init_app have this information available + if host != app.config["HOST"]: + app.config["HOST"] = host + + # set up kwargs dict for app.run + kwargs = { + "host": host, + "port": app.config["PORT"], + } + # watch templates folder to reload when something changes + extra_dirs = ["templates"] + extra_files = extra_dirs[:] + for extra_dir in extra_dirs: + for dirname, dirs, files in os.walk(extra_dir): + for filename in files: + filename = os.path.join(dirname, filename) + if os.path.isfile(filename): + extra_files.append(filename) + kwargs["extra_files"] = extra_files + + kwargs = configure_ssl(kwargs, app.config, ssl) + app.app_context().push() init_app(app, hwibridge=hwibridge) @@ -140,19 +163,6 @@ def server( toraddr_file = path.join(app.specter.data_folder, "onion.txt") - # watch templates folder to reload when something changes - extra_dirs = ["templates"] - extra_files = extra_dirs[:] - for extra_dir in extra_dirs: - for dirname, dirs, files in os.walk(extra_dir): - for filename in files: - filename = os.path.join(dirname, filename) - if os.path.isfile(filename): - extra_files.append(filename) - - kwargs = {"host": host, "port": app.config["PORT"], "extra_files": extra_files} - kwargs = configure_ssl(kwargs, app.config, ssl) - if hwibridge: if kwargs.get("ssl_context"): logger.error( diff --git a/src/cryptoadvance/specter/config.py b/src/cryptoadvance/specter/config.py index e31bdeed7b..b7e2c7fab3 100644 --- a/src/cryptoadvance/specter/config.py +++ b/src/cryptoadvance/specter/config.py @@ -57,6 +57,7 @@ class BaseConfig(object): # The prefix for extensions which don't get access to the session cookie (if SPECTER_URL_PREFIX isn't compromised) ISOLATED_CLIENT_EXT_URL_PREFIX = "/ext" + HOST = os.getenv("HOST", "127.0.0.1") PORT = os.getenv("PORT", 25441) CONNECT_TOR = _get_bool_env_var(os.getenv("CONNECT_TOR", "False")) SPECTER_DATA_FOLDER = os.path.expanduser( @@ -77,6 +78,13 @@ class BaseConfig(object): # CERT and KEY is for running self-signed-ssl-certs. Check cli_server for details CERT = os.getenv("CERT", None) KEY = os.getenv("KEY", None) + + # This will be used to search for a bitcoin.conf in order to enable the + # auth method "RPC password as pin" + RASPIBLITZ_SPECTER_RPC_LOGIN_BITCOIN_CONF_LOCATION = os.getenv( + "RASPIBLITZ_SPECTER_RPC_LOGIN_BITCOIN_CONF_LOCATION", "/mnt/hdd/bitcoin" + ) + # This will get passed to initialize the specter-object DEFAULT_SPECTER_CONFIG = {} @@ -176,6 +184,7 @@ class BaseConfig(object): "cryptoadvance.specterext.swan.service", "cryptoadvance.specterext.liquidissuer.service", "cryptoadvance.specterext.devhelp.service", + "cryptoadvance.specterext.notifications.service", "cryptoadvance.specterext.exfund.service", "cryptoadvance.specterext.faucet.service", "cryptoadvance.specterext.electrum.service", diff --git a/src/cryptoadvance/specter/device.py b/src/cryptoadvance/specter/device.py index de6f2b828a..b602c338cc 100644 --- a/src/cryptoadvance/specter/device.py +++ b/src/cryptoadvance/specter/device.py @@ -3,6 +3,7 @@ from cryptoadvance.specter.util.reflection import get_subclasses_for_clazz from .key import Key +from typing import List from .persistence import read_json_file, write_json_file import logging from .helpers import is_testnet, is_liquid @@ -44,7 +45,7 @@ def __init__(self, name, alias, keys, blinding_key, fullpath, manager): """ self.name = name self.alias = alias - self.keys = keys + self.keys: List[Key] = keys self.fullpath = fullpath self.blinding_key = blinding_key self.manager = manager diff --git a/src/cryptoadvance/specter/devices/jade.py b/src/cryptoadvance/specter/devices/jade.py index eafe44fa12..c5ba44c05f 100644 --- a/src/cryptoadvance/specter/devices/jade.py +++ b/src/cryptoadvance/specter/devices/jade.py @@ -11,7 +11,9 @@ class Jade(HWIDevice): qr_code_support = True supported_qr_code_format = "crypto-psbt" + qr_code_support_verify = True sd_card_support = False + supports_qr_message_signing = True supports_hwi_toggle_passphrase = False supports_hwi_multisig_display_address = True liquid_support = True diff --git a/src/cryptoadvance/specter/managers/config_manager.py b/src/cryptoadvance/specter/managers/config_manager.py index 29dbfbf655..49ff45f2c3 100644 --- a/src/cryptoadvance/specter/managers/config_manager.py +++ b/src/cryptoadvance/specter/managers/config_manager.py @@ -46,7 +46,7 @@ def __init__(self, data_folder, config={}): "asset_labels": { "liquidv1": {}, }, - "active_node_alias": "default", + "active_node_alias": None, "proxy_url": "socks5h://localhost:9050", # Tor proxy URL "only_tor": False, "tor_control_port": "", diff --git a/src/cryptoadvance/specter/managers/node_manager.py b/src/cryptoadvance/specter/managers/node_manager.py index f319d95452..b3883e1827 100644 --- a/src/cryptoadvance/specter/managers/node_manager.py +++ b/src/cryptoadvance/specter/managers/node_manager.py @@ -8,7 +8,7 @@ from ..specter_error import SpecterError, SpecterInternalException from ..persistence import PersistentObject, write_node, delete_file from ..helpers import alias, calc_fullpath, load_jsons -from ..node import Node +from ..node import Node, NonExistingNode from ..internal_node import InternalNode from ..services import callbacks from ..managers.service_manager import ServiceManager @@ -18,14 +18,11 @@ class NodeManager: - # chain is required to manage wallets when bitcoind is not running - DEFAULT_ALIAS = "default" - def __init__( self, proxy_url="socks5h://localhost:9050", only_tor=False, - active_node="default", + active_node=None, bitcoind_path="", internal_bitcoind_version="", data_folder="", @@ -56,6 +53,7 @@ def load_from_disk(self, data_folder=None): if not os.path.isdir(data_folder): os.mkdir(data_folder) nodes_files = load_jsons(self.data_folder, key="alias") + logger.debug(nodes_files) for node_alias in nodes_files: try: valid_node = True @@ -101,49 +99,15 @@ def load_from_disk(self, data_folder=None): port=7041, host="localhost", protocol="http", - default_alias=self.DEFAULT_ALIAS, ) - logger.debug( - "Creating an external BTC node with the initial configuration." - ) - self.add_external_node( - node_type="BTC", - name="Bitcoin Core", - autodetect=True, - datadir=get_default_datadir(), - user="", - password="", - port=8332, - host="localhost", - protocol="http", - default_alias=self.DEFAULT_ALIAS, - ) - - # Make sure we always have the default node - # (needed for the rpc-as-pin-authentication used on Raspiblitz) - has_default_node = False - for node in self.nodes.values(): - if node.alias == self.DEFAULT_ALIAS: - has_default_node = True - # Recreate the default node if it doesn't exist anymore - if not has_default_node: - logger.debug("Recreating the default node.") - self.add_external_node( - node_type="BTC", - name="Bitcoin Core", - autodetect=True, - datadir=get_default_datadir(), - user="", - password="", - port=8332, - host="localhost", - protocol="http", - default_alias=self.DEFAULT_ALIAS, - ) @property def active_node(self) -> Node: - return self.get_by_alias(self._active_node) + """returns the current active node or a NonExistingNode + if no node is active, currently. + """ + active_node = self.get_by_alias(self._active_node) + return active_node if active_node else NonExistingNode() @property def nodes_names(self) -> list: @@ -155,26 +119,37 @@ def nodes_by_chain(self, chain: str) -> list: return [node for node in self.nodes.values() if node.chain == chain] def switch_node(self, node_alias: str): - # This will throw an error if the node doesn't exist + """This will throw an SpecterError if the node doesn't exist. + It won't persist anything! Use specter.update_active_node to persist! + """ + new_node = self.get_by_alias(node_alias) + if not new_node: + raise SpecterError(f"Node alias {node_alias} does not exist!") logger.debug(f"Switching from {self._active_node} to {node_alias}.") - self._active_node = self.get_by_alias(node_alias).alias - - def default_node(self) -> Node: - return self.get_by_alias(self.DEFAULT_ALIAS) + self._active_node = node_alias def get_by_alias(self, alias: str) -> Node: + """Returns a Node instance for the given alias. + None if a node with that alias doesn't exist + """ for node in self.nodes.values(): if node.alias == alias: return node - raise SpecterError("Node alias %s does not exist!" % alias) + return None def get_by_name(self, name: str) -> Node: + """Returns a Node instance for the given alias. + raises an SpecterError if it doesn't exist + """ for node in self.nodes.values(): if node.name == name: return node raise SpecterError("Node name %s does not exist!" % name) def get_name_from_alias(self, alias: str) -> str: + """Returns the name for a specific node alias + raises an SpecterError if it doesn't exist + """ for node in self.nodes.values(): if node.alias == alias: return node.name @@ -200,26 +175,25 @@ def update_bitcoind_version(self, specter, version): def add_external_node( self, - node_type, - name, - autodetect, + node_type: str, + name: str, + autodetect: bool, datadir, - user, - password, - port, - host, - protocol, - default_alias=None, + user: str, + password: str, + port: str, + host: str, + protocol: str, ): - """Adding a node. Params: - :param node_type: only valid for autodetect. Either BTC or ELM + """Adding a node and saves it to disk as well. Params: + * node_type: only valid for autodetect. Either BTC or ELM + * name: A nice name for this node. The alias will get calculated out of that + * autodetect (boolean): whether this node should get autodetected + * datadir: questionable! Why is that here needed?! This should only be used for an external node. Use add_internal_node for internal node and if you have defined your own node type, use save_node directly to save the node (and create it yourself) """ - if not default_alias: - node_alias = alias(name) - else: - node_alias = default_alias + node_alias = alias(name) fullpath = os.path.join(self.data_folder, "%s.json" % node_alias) i = 2 while os.path.isfile(fullpath): @@ -247,6 +221,8 @@ def add_external_node( return node def save_node(self, node): + """writes the node to disk. Will also apply a fullpath based on the datadir of the + NodeManager if the node doesn't have one.""" if not hasattr(node, "fullpath"): node.fullpath = calc_fullpath(self.data_folder, node.alias) write_node(node, node.fullpath) @@ -254,20 +230,16 @@ def save_node(self, node): def add_internal_node( self, - name, + name: str, network="main", - port=None, - default_alias=None, + port: str = None, datadir=None, ): """Adding an internal node. Params: This should only be used for internal nodes. Use add__External_node for external nodes and if you have defined your own node-type, use save_node directly. to save the node (and create it yourself) """ - if not default_alias: - node_alias = alias(name) - else: - node_alias = default_alias + node_alias = alias(name) fullpath = os.path.join(self.data_folder, "%s.json" % node_alias) i = 2 while os.path.isfile(fullpath): @@ -298,6 +270,7 @@ def add_internal_node( return node def delete_node(self, node, specter): + """Deletes the node. Also from the disk.""" logger.info("Deleting {}".format(node.alias)) try: # Delete from wallet manager @@ -306,7 +279,7 @@ def delete_node(self, node, specter): delete_file(node.fullpath) delete_file(node.fullpath + ".bkp") # Update the active node - if self._active_node == node.alias: + if self._active_node == node.alias and len(self.nodes) > 0: specter.update_active_node( next(iter(self.nodes.values())).alias ) # This switches to the first node in the node list, which is usually the default node diff --git a/src/cryptoadvance/specter/managers/service_manager/service_manager.py b/src/cryptoadvance/specter/managers/service_manager/service_manager.py index 7798a1955e..cc17e817e9 100644 --- a/src/cryptoadvance/specter/managers/service_manager/service_manager.py +++ b/src/cryptoadvance/specter/managers/service_manager/service_manager.py @@ -19,7 +19,7 @@ from cryptoadvance.specter.util.specter_migrator import SpecterMigration -from ...services.service import Service +from ...services.service import Service, ServiceOptionality from ...services import callbacks, ExtensionException from ...util.reflection import ( _get_module_from_class, @@ -395,6 +395,16 @@ def delete_services_with_unencrypted_storage(self, user: User): self.specter.service_unencrypted_storage_manager.delete_all_service_data(user) logger.debug(f"Deleted unencrypted services") + def add_required_services_to_users(self, users, force_opt_out=False): + "Adds the mandatory and opt_out (only if no services activated for user) services to users" + for service in self.services.values(): + for user in users: + if service.optionality == ServiceOptionality.mandatory or ( + service.optionality == ServiceOptionality.opt_out + and ((service.id not in user.services) or force_opt_out) + ): + user.add_service(service.id) + @classmethod def get_service_x_dirs(cls, x): """returns a list of package-directories which each represents a specific service. diff --git a/src/cryptoadvance/specter/managers/wallet_manager.py b/src/cryptoadvance/specter/managers/wallet_manager.py index d44006a265..301ad8404b 100644 --- a/src/cryptoadvance/specter/managers/wallet_manager.py +++ b/src/cryptoadvance/specter/managers/wallet_manager.py @@ -13,7 +13,7 @@ from ..helpers import add_dicts, alias, is_liquid, load_jsons from ..liquid.wallet import LWallet from ..persistence import delete_folder -from ..rpc import RpcError, get_default_datadir +from ..rpc import RpcError, get_default_datadir, BrokenCoreConnectionException from ..specter_error import SpecterError, SpecterInternalException, handle_exception from ..util.flask import FlaskThread from ..wallet import ( # TODO: `purposes` unused here, but other files rely on this import @@ -31,7 +31,6 @@ class WalletManager: # chain is required to manage wallets when bitcoind is not running def __init__( self, - bitcoin_core_version_raw, data_folder, rpc, chain, @@ -50,8 +49,6 @@ def __init__( # key is the name of the wallet, value is the actual instance self.wallets = {} - # A way to communicate failed wallets to the outside - self.bitcoin_core_version_raw = bitcoin_core_version_raw self.allow_threading_for_testing = allow_threading_for_testing # define different wallet classes for liquid and bitcoin self.WalletClass = LWallet if is_liquid(chain) else Wallet @@ -249,7 +246,7 @@ def _update(self, wallets_update_list: Dict): ) self.wallets[wallet_name] = loaded_wallet except Exception as e: - handle_exception(e) + logger.exception(e) self._failed_load_wallets.append( { **wallets_update_list[wallet], @@ -263,14 +260,13 @@ def _update(self, wallets_update_list: Dict): # only ignore rpc errors except RpcError as e: logger.error(f"Failed updating wallet manager. RPC error: {e}") - logger.info("Updating wallet manager done. Result:") - logger.info(f" * loaded_wallets: {len(self.wallets)}") - logger.info(f" * failed_load_wallets: {len(self._failed_load_wallets)}") - for wallet in self._failed_load_wallets: - logger.info(f" * {wallet['name']} : {wallet['loading_error']}") - - wallets_update_list = {} - self.is_loading = False + finally: + self.is_loading = False + logger.info("Updating wallet manager done. Result:") + logger.info(f" * loaded_wallets: {len(self.wallets)}") + logger.info(f" * failed_load_wallets: {len(self._failed_load_wallets)}") + for wallet in self._failed_load_wallets: + logger.info(f" * {wallet['name']} : {wallet['loading_error']}") def get_by_alias(self, alias): for wallet_name in self.wallets: @@ -334,6 +330,16 @@ def wallets(self, value): self._wallets[self.chain] = {} self._wallets[self.chain] = value + @property + def bitcoin_core_version_raw(self): + try: + bitcoin_core_version_raw = self.rpc.getnetworkinfo()["version"] + return bitcoin_core_version_raw or 200000 + except BrokenCoreConnectionException: + # In good faith and in order to keep the tests running, we assume + # a reasonable core version + return 200000 + def create_wallet(self, name, sigs_required, key_type, keys, devices, **kwargs): try: walletsindir = [ diff --git a/src/cryptoadvance/specter/node.py b/src/cryptoadvance/specter/node.py index 7869c784b6..8bd7e815c1 100644 --- a/src/cryptoadvance/specter/node.py +++ b/src/cryptoadvance/specter/node.py @@ -28,7 +28,94 @@ logger = logging.getLogger(__name__) -class AbstractNode(PersistentObject): +class NonExistingNode(PersistentObject): + """A kind of Null-object as it represents a non-existing Node. It's deriving from PersistentObject but is not meant to be + persisted. Instead, it's be created on the fly from the NodeManager if it doesn't have a reasonable node available. + It also works as some kind of minimal implementation so that specter doesn't fail gracefully. + """ + + @property + def info(self): + return {} + + @property + def network_info(self): + return {} + + @property + def uptime(self): + return -1 + + @property + def chain(self): + return None + + @property + def bitcoin_core_version_raw(self): + return 9999999 + + def update_rpc(self): + pass + + @property + def is_running(self): + return False + + @property + def rpc(self): + return None + + def check_blockheight(self): + """check_blockheight is a method which is probably deprecated. + It should return True if there are new blocks available since check_info has been called + (which updates the cached _info[] dict) + """ + raise NotImplemented( + "A Node Implementation need to implement the check_blockheight method" + ) + + def is_device_supported(self, device_class_or_device_instance): + """Lets the node deactivate specific devices. The parameter could be a device or a device_type + You have to check yourself if overriding this method. + e.g. + if device_instance_or_device_class.__class__ == type: + device_class = device_instance_or_device_class + else: + device_class = device_instance_or_device_class.__class__ + # example: + # if BitcoinCore == device_class: + # return False + return True + """ + return True + + def node_info_template(self): + """This should return the path to a Info template as string""" + return "node/components/bitcoin_core_info.jinja" + + def node_logo_template(self): + """This should return the path to a Logo template as string + The template should contain the logo independent from the + status of the node. It's used in the node-selector + """ + return "includes/sidebar/components/node_logo.jinja" + + def node_connection_template(self): + """This should return the path to a connection template as string""" + return "includes/sidebar/components/node_connection.jinja" + + def delete_wallet_file(self, wallet) -> bool: + """Deleting the wallet file located on the node. This only works if the node is on the same machine as Specter. + Returns True if the wallet file could be deleted, otherwise returns False. + + In the case of an Abtract Node, we consider that method as an edge-case anyway and we just return False here. + That is the normal usage if you don't have access to the internals of your Bitcoin Core. + Overwrite as necessary. + """ + return False + + +class AbstractNode(NonExistingNode): """This is a Node class worth deriving from. It tries to define as many attributes as possible which are needed but probably in a very inefficient way, e.g. without any caching. Feel free to improve that in subclasses and you might get inspired by existing sublasses """ @@ -128,55 +215,6 @@ def is_running(self): else: return True - def check_blockheight(self): - """check_blockheight is a method which is probably deprecated. - It should return True if there are new blocks available since check_info has been called - (which updates the cached _info[] dict) - """ - raise NotImplemented( - "A Node Implementation need to implement the check_blockheight method" - ) - - def is_device_supported(self, device_class_or_device_instance): - """Lets the node deactivate specific devices. The parameter could be a device or a device_type - You have to check yourself if overriding this method. - e.g. - if device_instance_or_device_class.__class__ == type: - device_class = device_instance_or_device_class - else: - device_class = device_instance_or_device_class.__class__ - # example: - # if BitcoinCore == device_class: - # return False - return True - """ - return True - - def node_info_template(self): - """This should return the path to a Info template as string""" - return "node/components/bitcoin_core_info.jinja" - - def node_logo_template(self): - """This should return the path to a Logo template as string - The template should contain the logo independent from the - status of the node. It's used in the node-selector - """ - return "includes/sidebar/components/node_logo.jinja" - - def node_connection_template(self): - """This should return the path to a connection template as string""" - return "includes/sidebar/components/node_connection.jinja" - - def delete_wallet_file(self, wallet) -> bool: - """Deleting the wallet file located on the node. This only works if the node is on the same machine as Specter. - Returns True if the wallet file could be deleted, otherwise returns False. - - In the case of an Abtract Node, we consider that method as an edge-case anyway and we just return False here. - That is the normal usage if you don't have access to the internals of your Bitcoin Core. - Overwrite as necessary. - """ - return False - class Node(AbstractNode): """A Node represents the connection to a Bitcoin and/or Liquid (Full-) node. diff --git a/src/cryptoadvance/specter/rpc.py b/src/cryptoadvance/specter/rpc.py index f259e664f5..af054d9e7a 100644 --- a/src/cryptoadvance/specter/rpc.py +++ b/src/cryptoadvance/specter/rpc.py @@ -496,7 +496,8 @@ def fn(*args, **kwargs): r = self.multi([(method, *args)], **kwargs)[0] if r["error"] is not None: raise RpcError( - f"Request error for method {method}: {r['error']['message']}", r + f"Request error for method {method}{args}: {r['error']['message']}", + r, ) return r["result"] diff --git a/src/cryptoadvance/specter/server.py b/src/cryptoadvance/specter/server.py index c2c6dec490..b1d8800855 100644 --- a/src/cryptoadvance/specter/server.py +++ b/src/cryptoadvance/specter/server.py @@ -162,6 +162,13 @@ def init_app(app: SpecterFlask, hwibridge=False, specter=None): specter=specter, devstatus_threshold=app.config["SERVICES_DEVSTATUS_THRESHOLD"] ) + def service_manager_cleanup_on_exit(signum, frame): + return specter.service_manager.execute_ext_callbacks( + callbacks.cleanup_on_exit, signum, frame + ) + + specter.call_functions_at_cleanup_on_exit.append(service_manager_cleanup_on_exit) + specter.initialize() # HWI @@ -206,9 +213,17 @@ def login(id, password: str = None): from cryptoadvance.specter.server_endpoints import controller from cryptoadvance.specter.services import controller as serviceController + # this number of view_functions needs to be updated by hand when some are added or removed. + number_of_expected_view_functions = 105 if app.config.get("TESTING"): - logger.info(f"We have {len(app.view_functions)} view Functions") - if app.config.get("TESTING") and len(app.view_functions) <= 51: + logger.info( + f"We have {len(app.view_functions)} view Functions. " + f"There should be {number_of_expected_view_functions}." + ) + if ( + app.config.get("TESTING") + and len(app.view_functions) < number_of_expected_view_functions + ): # Need to force a reload as otherwise the import is skipped # in pytest, the app is created anew for each test # But we shouldn't do that if not necessary as this would result in @@ -277,6 +292,7 @@ def every5seconds(): scheduler.init_app(app) scheduler.start() + specter.service_manager.add_required_services_to_users(specter.user_manager.users) logger.info("----> starting service callback_after_serverpy_init_app ") specter.service_manager.execute_ext_callbacks( after_serverpy_init_app, scheduler=scheduler diff --git a/src/cryptoadvance/specter/server_endpoints/__init__.py b/src/cryptoadvance/specter/server_endpoints/__init__.py index cf00765ad6..d354a54a5a 100644 --- a/src/cryptoadvance/specter/server_endpoints/__init__.py +++ b/src/cryptoadvance/specter/server_endpoints/__init__.py @@ -1,4 +1,6 @@ from flask import flash as flask_flash +from flask import current_app as app +from ..services import callbacks def flash(*args, **kwargs): @@ -6,4 +8,11 @@ def flash(*args, **kwargs): This function could be placed in util but as it might use the service_manager, we place it here for now. """ - flask_flash(*args, **kwargs) + + return_values = app.specter.service_manager.execute_ext_callbacks( + callbacks.flash, *args, **kwargs + ) + + # if no extension handled the callback + if not return_values: + flask_flash(*args, **kwargs) diff --git a/src/cryptoadvance/specter/server_endpoints/auth.py b/src/cryptoadvance/specter/server_endpoints/auth.py index 8f59311a25..022abe1e9b 100644 --- a/src/cryptoadvance/specter/server_endpoints/auth.py +++ b/src/cryptoadvance/specter/server_endpoints/auth.py @@ -7,10 +7,13 @@ from flask_babel import lazy_gettext as _ from flask_login import current_user, login_required, logout_user +from cryptoadvance.specter.specter import Specter + from ..helpers import alias from ..server_endpoints import flash from ..services import ExtensionException from ..user import User, hash_password, verify_password +from ..rpc import BitcoinRPC, _detect_rpc_confs_via_datadir rand = random.randint(0, 1e32) # to force style refresh last_sensitive_request = 0 # to rate limit sensitive requests @@ -28,60 +31,13 @@ def login(): auth = app.specter.config["auth"] if auth["method"] == "none": - app.login("admin") - app.logger.info("AUDIT: Successful Login no credentials") - return redirect_login(request) - - if auth["method"] == "rpcpasswordaspin": - # TODO: check the password via RPC-call - if ( - app.specter.default_node.rpc is None - or not app.specter.default_node.rpc.test_connection() - ): - if app.specter.default_node.password == request.form["password"]: - app.login("admin", request.form["password"]) - app.logger.info( - "AUDIT: Successfull Login via RPC-credentials (node disconnected)" - ) - return redirect_login(request) - - flash( - _( - "We could not check your password, maybe Bitcoin Core is not running or not configured?" - ), - "error", - ) - app.logger.info("AUDIT: Failed to check password") - return ( - render_template( - "login.jinja", - specter=app.specter, - data={"controller": "controller.login"}, - ), - 401, - ) - rpc = app.specter.default_node.rpc.clone() - rpc.password = request.form["password"] - if rpc.test_connection(): - app.login("admin", request.form["password"]) - app.logger.info("AUDIT: Successfull Login via RPC-credentials") - return redirect_login(request) - + return login_method_none() + elif auth["method"] == "rpcpasswordaspin": + return login_method_rpcpasswordaspin() elif auth["method"] == "passwordonly": - password = request.form["password"] - if verify_password(app.specter.user_manager.admin.password_hash, password): - app.login("admin", request.form["password"]) - return redirect_login(request) - + return login_method_passwordonly() elif auth["method"] == "usernamepassword": - # TODO: This way both "User" and "user" will pass as usernames, should there be strict check on that here? Or should we keep it like this? - username = request.form["username"] - password = request.form["password"] - user = app.specter.user_manager.get_user_by_username(username) - if user: - if verify_password(user.password_hash, password): - app.login(user.id, request.form["password"]) - return redirect_login(request) + return login_method_usernamepassword() # Either invalid method or incorrect credentials flash(_("Invalid username or password"), "error") @@ -103,6 +59,83 @@ def login(): ) +def deny_login(comment=""): + flash("Invalid user or password!") + app.logger.info("AUDIT: Invalid password login attempt") + return redirect("login") + + +def login_method_none(): + app.login("admin") + app.logger.info("AUDIT: Successful Login no credentials") + return redirect_login(request) + + +def login_method_rpcpasswordaspin(): + # This authentiaction method has been especially created for Raspiblitz + # We assume here, that no preconfigured node-connection is available + # otherwise, which configured node should we use? + # We want to get rid of the default node, so we can't use that + # Instead we use a default location where we assume the bitcoin.conf + # This can be overridden via ENV_var. + + specter: Specter = app.specter + if specter.node and specter.node._get_rpc().test_connection(): + rpc = specter.node._get_rpc().clone() + else: + confs = _detect_rpc_confs_via_datadir( + None, + datadir=app.config["RASPIBLITZ_SPECTER_RPC_LOGIN_BITCOIN_CONF_LOCATION"], + ) + if len(confs) == 0: + flash( + "No RPC connection to Bitcoin Core found. Cannot Log you in.", "error" + ) + return redirect(url_for("login")) + conf = confs[0] + rpc = BitcoinRPC(**conf) + + # A bit redundant as this has been checked before but let's be sure + if not rpc.test_connection(): + flash( + "It seems that there is no working RPC connection to Bitcoin Core. Cannot Log you in." + ) + return redirect(url_for("login")) + orig_password = rpc.password + rpc.password = request.form["password"] + if rpc.password == request.form["password"] and rpc.test_connection(): + app.login("admin", request.form["password"]) + app.logger.info("AUDIT: Successfull Login via RPC-credentials") + return redirect_login(request) + if orig_password == request.form["password"]: + app.login("admin", request.form["password"]) + app.logger.info( + f"AUDIT: Successfull Login via RPC-credentials (node not reachable) {rpc.password}" + ) + return redirect_login(request) + return deny_login(comment="Pin") + + +def login_method_passwordonly(): + password = request.form["password"] + if verify_password(app.specter.user_manager.admin.password_hash, password): + app.login("admin", request.form["password"]) + return redirect_login(request) + return deny_login(comment="passwordonly") + + +def login_method_usernamepassword(): + # TODO: This way both "User" and "user" will pass as usernames, should there be strict check on that here? Or should we keep it like this? + username = request.form["username"] + password = request.form["password"] + user = app.specter.user_manager.get_user_by_username(username) + if user: + if verify_password(user.password_hash, password): + app.login(user.id, request.form["password"]) + return redirect_login(request) + return deny_login(comment="usernamepassword") + + @auth_endpoint.route("/register", methods=["GET", "POST"]) def register(): """register""" @@ -149,6 +182,7 @@ def register(): plaintext_password=password, config=config, ) + app.specter.service_manager.add_required_services_to_users([user]) flash( _( diff --git a/src/cryptoadvance/specter/server_endpoints/nodes.py b/src/cryptoadvance/specter/server_endpoints/nodes.py index 06dcd3b194..940c75f7fd 100644 --- a/src/cryptoadvance/specter/server_endpoints/nodes.py +++ b/src/cryptoadvance/specter/server_endpoints/nodes.py @@ -121,22 +121,10 @@ def node_settings(node_alias): elif action == "forget": if not node_alias: flash(_("Failed to deleted node. Node isn't saved"), "error") - elif len(app.specter.node_manager.nodes) > 1: + else: app.specter.node_manager.delete_node(node, app.specter) flash(_("Node deleted successfully")) - return redirect( - url_for( - "nodes_endpoint.node_settings", - node_alias=app.specter.node.alias, - ) - ) - else: - flash( - _( - "Failed to delete node. Specter must have at least one node configured" - ), - "error", - ) + return redirect(url_for("nodes_endpoint.node_settings")) elif action == "test": # If this is failing, the test_rpc-method needs improvement # Don't wrap this into a try/except otherwise the feedback diff --git a/src/cryptoadvance/specter/server_endpoints/wallets/wallets.py b/src/cryptoadvance/specter/server_endpoints/wallets/wallets.py index 3e08f76f43..5234d74213 100644 --- a/src/cryptoadvance/specter/server_endpoints/wallets/wallets.py +++ b/src/cryptoadvance/specter/server_endpoints/wallets/wallets.py @@ -444,7 +444,7 @@ def history(wallet_alias): wallet.update_balance() wallet.check_utxo() - renderting = render_template( + return render_template( "wallet/history/wallet_history.jinja", wallet_alias=wallet_alias, wallet=wallet, @@ -453,8 +453,6 @@ def history(wallet_alias): rand=rand, services=app.specter.service_manager.services, ) - logger.info("-------------------end render_template()") - return renderting ###### Wallet receive ###### diff --git a/src/cryptoadvance/specter/services/callbacks.py b/src/cryptoadvance/specter/services/callbacks.py index 027f24bf25..fe7de24c95 100644 --- a/src/cryptoadvance/specter/services/callbacks.py +++ b/src/cryptoadvance/specter/services/callbacks.py @@ -57,3 +57,19 @@ Will get called right after having access to app.specter """ specter_added_to_flask_app = "specter_added_to_flask_app" + +""" + Will get called when the server_endpoints.flash is called +""" +flash = "flash" + +""" + Callback that is not used yet, but could be implmented in server_endpoints just as flash +""" +create_and_show_notification = "create_and_show_notification" + + +""" + Callback that is called last in specter.cleanup_on_exit() +""" +cleanup_on_exit = "cleanup_on_exit" diff --git a/src/cryptoadvance/specter/services/service.py b/src/cryptoadvance/specter/services/service.py index f81340306f..811f724b16 100644 --- a/src/cryptoadvance/specter/services/service.py +++ b/src/cryptoadvance/specter/services/service.py @@ -25,6 +25,12 @@ devstatus_prod = "prod" +class ServiceOptionality: + mandatory = "mandatory" # mandatory is not used yet by any service. Before it can be used it has to be clarified what happens in delete_services_with_encrypted_storage of mandatory services + opt_in = "opt_in" + opt_out = "opt_out" + + class Service: """A base class for Services""" @@ -38,6 +44,8 @@ class Service: # If the blueprint gets a "/ext" prefix (isolated_client = True), the login cookie won't work for all specter core functionality isolated_client = True devstatus = devstatus_alpha + optionality = ServiceOptionality.opt_in + visible_in_sidebar = True encrypt_data = False def __init__(self, active, specter): diff --git a/src/cryptoadvance/specter/specter.py b/src/cryptoadvance/specter/specter.py index a615b6d74b..43ec69e4ee 100644 --- a/src/cryptoadvance/specter/specter.py +++ b/src/cryptoadvance/specter/specter.py @@ -40,6 +40,7 @@ from .process_controller.bitcoind_controller import BitcoindPlainController from .rpc import BitcoinRPC, RpcError, get_default_datadir from .services.service import devstatus_alpha, devstatus_beta, devstatus_prod +from .services import callbacks from .specter_error import ExtProcTimeoutException, SpecterError from .tor_daemon import TorDaemonController from .user import User @@ -79,6 +80,7 @@ def __init__( if not os.path.isdir(data_folder): os.makedirs(data_folder) + self.call_functions_at_cleanup_on_exit = [] self.data_folder = data_folder self._config = config self._internal_bitcoind_version = internal_bitcoind_version @@ -127,10 +129,8 @@ def initialize(self): ) except SpecterError as e: if str(e).endswith("does not exist!"): - logger.warning( - f"Current Node doesn't exist. Switching over to node {self.node_manager.DEFAULT_ALIAS}." - ) - self.update_active_node(self.node_manager.DEFAULT_ALIAS) + if len(self.node_manager.nodes) > 0: + self.update_active_node(next(iter(self.nodes.values())).alias) else: raise e @@ -189,6 +189,7 @@ def initialize(self): signal.signal(signal.SIGINT, self.cleanup_on_exit) # This is for kill $pid --> SIGTERM signal.signal(signal.SIGTERM, self.cleanup_on_exit) + # a list of functions that are called at cleanup_on_exit taking in each signum, frame def cleanup_on_exit(self, signum=0, frame=0): if self._tor_daemon: @@ -199,6 +200,9 @@ def cleanup_on_exit(self, signum=0, frame=0): if not node.external_node: node.stop() + for f in self.call_functions_at_cleanup_on_exit: + f(signum, frame) + logger.info("Closing Specter after cleanup") # For some reason we need to explicitely exit here. Otherwise it will hang exit(0) @@ -237,13 +241,7 @@ def check(self, user=None, check_all=False): @property def node(self) -> Node: - try: - return self.node_manager.active_node - except SpecterError as e: - logger.error("SpecterError while accessing active_node") - logger.exception(e) - self.update_active_node(list(self.node_manager.nodes.values())[0].alias) - return self.node_manager.active_node + return self.node_manager.active_node @property def default_node(self): @@ -565,7 +563,7 @@ def user_config(self): @property def active_node_alias(self): - return self.user_config.get("active_node_alias", "default") + return self.user_config.get("active_node_alias", None) @property def explorer(self): @@ -745,7 +743,7 @@ def migrate_old_node_format(self): old_internal_rpc = self.config.get("internal_node", None) if old_internal_rpc and os.path.isfile(self.bitcoind_path): internal_node = InternalNode( - "Specter Bitcoin", + "Specter Bitcoin internal", "specter_bitcoin", old_internal_rpc.get("autodetect", False), old_internal_rpc.get("datadir", get_default_datadir()), @@ -755,7 +753,7 @@ def migrate_old_node_format(self): old_internal_rpc.get("host", "localhost"), old_internal_rpc.get("protocol", "http"), os.path.join( - os.path.join(self.data_folder, "nodes"), "specter_bitcoin.json" + os.path.join(self.data_folder, "nodes"), "bitcoin_core.json" ), self, self.bitcoind_path, @@ -776,7 +774,7 @@ def migrate_old_node_format(self): if old_rpc: node = Node( "Bitcoin Core", - "default", + "bitcoin_core", old_rpc.get("autodetect", True), old_rpc.get("datadir", get_default_datadir()), old_rpc.get("user", ""), @@ -784,16 +782,21 @@ def migrate_old_node_format(self): old_rpc.get("port", None), old_rpc.get("host", "localhost"), old_rpc.get("protocol", "http"), - os.path.join(os.path.join(self.data_folder, "nodes"), "default.json"), + os.path.join( + os.path.join(self.data_folder, "nodes"), "bitcoin_core.json" + ), "BTC", self, ) logger.info(f"persisting {node} in migrate_old_node_format") write_node( node, - os.path.join(os.path.join(self.data_folder, "nodes"), "default.json"), + os.path.join( + os.path.join(self.data_folder, "nodes"), "bitcoin_core.json" + ), ) del self.config["rpc"] + self.config_manager.update_active_node("bitcoin_core") self._save() diff --git a/src/cryptoadvance/specter/specter_error.py b/src/cryptoadvance/specter/specter_error.py index 187a534813..4f84515e30 100644 --- a/src/cryptoadvance/specter/specter_error.py +++ b/src/cryptoadvance/specter/specter_error.py @@ -48,8 +48,13 @@ def get_logger_friendly(self): def handle_exception(exception, user=None): """prints the exception and most important the stacktrace""" - if app.config["SPECTER_CONFIGURATION_CLASS_FULLNAME"].endswith("DevelopmentConfig"): - raise exception + try: + if app.config["SPECTER_CONFIGURATION_CLASS_FULLNAME"].endswith( + "DevelopmentConfig" + ): + raise exception + except RuntimeError: # Application context might be missing + pass logger.error("Unexpected error:") logger.error( "----START-TRACEBACK-----------------------------------------------------------------" diff --git a/src/cryptoadvance/specter/static/styles.css b/src/cryptoadvance/specter/static/styles.css index dfe953ff81..40b476874f 100644 --- a/src/cryptoadvance/specter/static/styles.css +++ b/src/cryptoadvance/specter/static/styles.css @@ -17,6 +17,7 @@ html, body{ --cmap-border-highlight:#4B8CD8; --cmap-border-darker: #405062; --cmap-bg-address-is-mine:#306d30; + --cmap-bg-highlight: #4a90e2; margin: 0; padding: 0; @@ -222,7 +223,7 @@ nav.side > .item, nav.side > div > .item{ /*font-size: 0.85em;*/ } nav.side > a.item.active, nav.side > div > a.item.active{ - border-left: 3px solid #4A90E2; + border-left: 3px solid var(--cmap-bg-highlight); background: rgba(0,0,0,0.1); /*color: #fff;*/ } @@ -1278,31 +1279,38 @@ input:checked + .slider:before { transform: translateX(26px); } -/************** Styles for recipient-box ********************************/ + + + + + +/*****************************/ +/* Recipient Box +/*****************************/ .recipient_wrapper{ width: 100%; border-radius: 3px; box-shadow: 0px 10px 15px rgba(0,0,0,0.1); - } +} .recipient_wrapper_item{ border-radius: 5px; border: 1.5px solid var(--default-color); padding: 5px; margin-bottom: 5px; - } - .recipient_wrapper_inner_box{ +} +.recipient_wrapper_inner_box{ padding: 5px; margin-top: 5px; - } +} .recipient_button{ - background: var(--cmap-border); - padding-left: 2px; - padding-right: 2px; - margin-left: 2px; - margin-right: 2px; - border-radius: 3px; - height: 22px; - border: none; + background: var(--cmap-border); + padding-left: 2px; + padding-right: 2px; + margin-left: 2px; + margin-right: 2px; + border-radius: 3px; + height: 22px; + border: none; } .recipient_button:hover { background: var(--cmap-border-darker); diff --git a/src/cryptoadvance/specter/templates/base.jinja b/src/cryptoadvance/specter/templates/base.jinja index 6f910be2f4..f3cd4188db 100644 --- a/src/cryptoadvance/specter/templates/base.jinja +++ b/src/cryptoadvance/specter/templates/base.jinja @@ -155,6 +155,7 @@ {% endif %} {% endif %} + {% block scripts %} diff --git a/src/cryptoadvance/specter/templates/includes/sidebar/components/node_connection.jinja b/src/cryptoadvance/specter/templates/includes/sidebar/components/node_connection.jinja index 625626afd9..c62c9a16fe 100644 --- a/src/cryptoadvance/specter/templates/includes/sidebar/components/node_connection.jinja +++ b/src/cryptoadvance/specter/templates/includes/sidebar/components/node_connection.jinja @@ -1,5 +1,11 @@ {% set node = specter.node %} {% if node.is_running %} + {% if specter.info.get("initialblockdownload") %} +


{{ _("Bitcoin Core is still syncing...")}}
{{ _('(data might be outdated)')}}

+ {% endif %} + {% if specter.bitcoin_core_version_raw < 200000 %} +


{{ _("Bitcoin Core version is outdated.")}}
{{ _('Some features might not work...')}}
{{ _("(minimum required: v20.0.0).") }}

+ {% endif %}
{% include node.node_logo_template() %} {% include "includes/sidebar/components/bitcoin_core_info.jinja" %} diff --git a/src/cryptoadvance/specter/templates/includes/sidebar/sidebar.jinja b/src/cryptoadvance/specter/templates/includes/sidebar/sidebar.jinja index fdf2f9fe91..1dce255afa 100644 --- a/src/cryptoadvance/specter/templates/includes/sidebar/sidebar.jinja +++ b/src/cryptoadvance/specter/templates/includes/sidebar/sidebar.jinja @@ -63,12 +63,6 @@
{% endif %}
- {% if specter.node_manager.nodes | length > 1 and node_alias %} + {% if specter.node_manager.nodes | length > 0 and node_alias %} {% endif %} diff --git a/src/cryptoadvance/specter/templates/services/sidebar_services_list.jinja b/src/cryptoadvance/specter/templates/services/sidebar_services_list.jinja index 8eba07899a..ce1eb76b66 100644 --- a/src/cryptoadvance/specter/templates/services/sidebar_services_list.jinja +++ b/src/cryptoadvance/specter/templates/services/sidebar_services_list.jinja @@ -3,7 +3,7 @@
{% for _,plugin in specter.service_manager.services.items() %} - {% if plugin.id in current_user.services %} + {% if plugin.id in current_user.services and plugin.visible_in_sidebar %}  {{ plugin.name }} diff --git a/src/cryptoadvance/specter/txlist.py b/src/cryptoadvance/specter/txlist.py index 4d8f42b11f..1cf3327505 100644 --- a/src/cryptoadvance/specter/txlist.py +++ b/src/cryptoadvance/specter/txlist.py @@ -16,6 +16,7 @@ from .specter_error import SpecterError, SpecterInternalException from embit.descriptor import Descriptor from embit.liquid.descriptor import LDescriptor +from .util.common import str2bool from .util.psbt import ( AbstractTxContext, SpecterInputScope, @@ -24,6 +25,7 @@ SpecterTx, ) from .util.tx import decoderawtransaction +from threading import RLock logger = logging.getLogger(__name__) @@ -84,6 +86,7 @@ class TxItem(dict, AbstractTxListContext): "vsize", "address", ] + # type_converter will be used to _read_csv to have a proper mapping type_converter = [ str, int, @@ -289,13 +292,14 @@ def __dict__(self): class WalletAwareTxItem(TxItem): PSBTCls = SpecterPSBT + + # Columns for writing CSVs, type_converter for reading columns = TxItem.columns.copy() columns.extend( ["category", "flow_amount", "utxo_amount", "ismine"], ) - type_converter = TxItem.type_converter.copy() - type_converter.extend([str, float, float, bool]) + type_converter.extend([str, float, float, str2bool]) def __init__(self, parent, addresses, rawdir, **kwargs): super().__init__(parent, addresses, rawdir, **kwargs) @@ -323,6 +327,24 @@ def psbt(self) -> SpecterPSBT: self._psbt.update(updated) return self._psbt + @property + def is_taproot(self): + return str(self.descriptor).startswith("tr(") + + def decode_psbt(self, mode="embit") -> SpecterPSBT: + """Utility function which decodes this tx as psbt + as in the core rpc-call 'decodepsbt'. + However, it uses embit to calculate the details + use mode=core to ask core directly. + embit might support taproot, core might not. + """ + if mode == "core": + return self.rpc.decodepsbt(str(self.psbt)) + elif mode == "embit": + return self.psbt.to_dict() + else: + raise SpecterInternalException("Mode not existing") + @property def category(self): """One of mixed (default), generate, selftransfer, receive or send""" @@ -385,8 +407,11 @@ def flow_amount(self) -> float: def ismine(self) -> bool: if self.get("ismine"): return self["ismine"] - inputs = self.psbt.inputs - outputs = self.psbt.outputs + if self.is_taproot: + # This is a bug mitigation, see #2078 + return True + inputs: List[SpecterInputScope] = self.psbt.inputs + outputs: List[SpecterOutputScope] = self.psbt.outputs any_inputs_mine = any([inp.is_mine for inp in inputs]) any_outputs_mine = any([out.is_mine for out in outputs]) self["ismine"] = any_inputs_mine or any_outputs_mine @@ -417,13 +442,13 @@ def address(self): self["address"] = addresses[0] return self["address"] - def __dict__(self): - super_dict = dict(self) - super_dict["category"] = self.category - super_dict["flow_amount"] = self.flow_amount - super_dict["utxo_amount"] = self.utxo_amount - super_dict["ismine"] = (self["ismine"] or self.ismine,) - return super_dict + # def __dict__(self): + # super_dict = dict(self) + # super_dict["category"] = self.category + # super_dict["flow_amount"] = self.flow_amount + # super_dict["utxo_amount"] = self.utxo_amount + # super_dict["ismine"] = (self["ismine"] or self.ismine,) + # return super_dict class TxList(dict, AbstractTxListContext): @@ -432,6 +457,8 @@ class TxList(dict, AbstractTxListContext): ItemCls = WalletAwareTxItem # for inheritance PSBTCls = SpecterPSBT + lock = RLock() + def __init__(self, path, parent, addresses): self.parent = parent self.path = path @@ -474,6 +501,8 @@ def clear_cache(self): tx.clear_cache() delete_file(self.path) self._file_exists = False + self.clear() + logger.info(f"Cleared the Cache for {self.path} (and rawdir)") def getfetch(self, txid): @@ -504,7 +533,11 @@ def get_transactions(self, current_blockheight=None) -> WalletAwareTxItem: # Make a copy of all txs if the tx.ismine (which should be all of them) # As TxItem is derived from Dict, the __Dict__ will return a TxItem - transactions: List(TxItem) = [tx.copy() for tx in self.values() if tx.ismine] + with self.lock: + tx_values = self.values() + transactions: List(TxItem) = [ + tx.copy() for tx in self.values() if tx.ismine + ] # 1. sorted transactions = sorted(transactions, key=lambda tx: tx["time"], reverse=True) @@ -588,42 +621,43 @@ def add(self, txs): } (format of listtransactions) """ - # here we store all addresses in transactions - # to set them used later - addresses = [] - # first we add all transactions to cache - for txid in txs: - tx = txs[txid] - # find minimal from 3 times: - maxtime = 10445238000 # TODO: change after 31 dec 2300 lol - time = min( - tx.get("blocktime", maxtime), - tx.get("timereceived", maxtime), - tx.get("time", maxtime), - ) - obj = { - "txid": txid, - "fee": tx.get("fee", None), - "blockheight": tx.get("blockheight", None), - "blockhash": tx.get("blockhash", None), - "time": time, - "blocktime": tx.get("blocktime", None), - "conflicts": tx.get("walletconflicts", []), - "bip125-replaceable": tx.get("bip125-replaceable", "no"), - "hex": tx.get("hex", None), - } - txitem = self.ItemCls(self, self._addresses, self.rawdir, **obj) - self[txid] = txitem - if txitem.tx: - for vout in txitem.tx.vout: - try: - addr = vout.script_pubkey.address(get_network(self.chain)) - if addr not in addresses: - addresses.append(addr) - except: - pass # maybe not an address, but a raw script? - self._addresses.set_used(addresses) - self._save() + with self.lock: + # here we store all addresses in transactions + # to set them used later + addresses = [] + # first we add all transactions to cache + for txid in txs: + tx = txs[txid] + # find minimal from 3 times: + maxtime = 10445238000 # TODO: change after 31 dec 2300 lol + time = min( + tx.get("blocktime", maxtime), + tx.get("timereceived", maxtime), + tx.get("time", maxtime), + ) + obj = { + "txid": txid, + "fee": tx.get("fee", None), + "blockheight": tx.get("blockheight", None), + "blockhash": tx.get("blockhash", None), + "time": time, + "blocktime": tx.get("blocktime", None), + "conflicts": tx.get("walletconflicts", []), + "bip125-replaceable": tx.get("bip125-replaceable", "no"), + "hex": tx.get("hex", None), + } + txitem = self.ItemCls(self, self._addresses, self.rawdir, **obj) + self[txid] = txitem + if txitem.tx: + for vout in txitem.tx.vout: + try: + addr = vout.script_pubkey.address(get_network(self.chain)) + if addr not in addresses: + addresses.append(addr) + except: + pass # maybe not an address, but a raw script? + self._addresses.set_used(addresses) + self._save() def load(self, arr): """ diff --git a/src/cryptoadvance/specter/user.py b/src/cryptoadvance/specter/user.py index 1f7ae9bfa5..28aef4d765 100644 --- a/src/cryptoadvance/specter/user.py +++ b/src/cryptoadvance/specter/user.py @@ -293,7 +293,6 @@ def check_wallet_manager(self): ): wallet_manager = WalletManager( - self.specter.bitcoin_core_version_raw, wallets_folder, self.specter.rpc, self.specter.chain, diff --git a/src/cryptoadvance/specter/util/common.py b/src/cryptoadvance/specter/util/common.py index d5cf555472..a071bde31e 100644 --- a/src/cryptoadvance/specter/util/common.py +++ b/src/cryptoadvance/specter/util/common.py @@ -4,6 +4,7 @@ import json from flask_babel.speaklater import LazyString from typing import Union +from distutils.util import strtobool logger = logging.getLogger(__name__) @@ -12,11 +13,9 @@ def str2bool(my_str): """returns a reasonable boolean from a string so that "False" will result in False""" if my_str is None: return False - elif isinstance(my_str, str) and my_str.lower() == "false": - return False - elif isinstance(my_str, str) and my_str.lower() == "off": - return False - return bool(my_str) + elif isinstance(my_str, bool): + return my_str + return bool(strtobool(my_str)) def camelcase2snake_case(name): diff --git a/src/cryptoadvance/specter/wallet.py b/src/cryptoadvance/specter/wallet.py index bf4066000d..6920334d9e 100644 --- a/src/cryptoadvance/specter/wallet.py +++ b/src/cryptoadvance/specter/wallet.py @@ -875,7 +875,7 @@ def check_utxo(self): except Exception as e: logger.exception(e) self._full_utxo = [] - raise SpecterError(f"Failed to load utxos, {e}") + raise SpecterError(f"Failed to load utxos, {type(e).__name__}: {e}") def check_utxo_orig(self): """fetches the utxo-set from core and stores the result in self.__full_utxo which is diff --git a/src/cryptoadvance/specterext/devhelp/templates/devhelp/dev-console.jinja b/src/cryptoadvance/specterext/devhelp/templates/devhelp/dev-console.jinja index b3b5dbc64b..2f9389791d 100644 --- a/src/cryptoadvance/specterext/devhelp/templates/devhelp/dev-console.jinja +++ b/src/cryptoadvance/specterext/devhelp/templates/devhelp/dev-console.jinja @@ -27,9 +27,10 @@

- you can also do that in any javascript.console with: + Press F12 to open the dev-console (helpful to see objects)
+ You can also do that in any javascript.console with:
-    await pythonCommand("app.specter") 
+    await pythonCommand('app.specter') 
     
Some usefull idioms:
@@ -109,6 +110,7 @@
 
 
             } else {
+                console.log(result)
                 if (result == "") {
                     result = "(empty string)"
                 }
diff --git a/src/cryptoadvance/specterext/devhelp/templates/devhelp/html/notification-system.jinja b/src/cryptoadvance/specterext/devhelp/templates/devhelp/html/notification-system.jinja
new file mode 100644
index 0000000000..2c56b27d3e
--- /dev/null
+++ b/src/cryptoadvance/specterext/devhelp/templates/devhelp/html/notification-system.jinja
@@ -0,0 +1,136 @@
+{% extends "base.jinja" %}
+
+{% block main %}
+
+

Notification System

+
+

+ The notification System is a flexible way to Notify the user. Other than flask's flash, it's working instantly via WebSockets + and takes advantage of Web Api Notifications. + It has been introduced with #1766 +

+

+ It can e.g. be used in javascript like this(see below for the options): +

+ + + +

+ In python, you would rather use somehing like: +

+ + +
+ +

+ +
+

+ Let's create a Notification: +

+
+ +
+ The Title of the message: +
+

+ +
+ The Text of the message: +
+

+ +
+ {{ _("The target_uis") }}: + + +
    +
  • js_message_box the default
  • +
  • webapi uses Web Api Notifications
  • +
  • js_console same as console.log("bla")
  • +
  • flash same as flash("some message")
  • +
  • logging same as logger.info("Some message")
  • +
+
+
+
+ +

+ +
+ {{ _("The Notification Type") }}: + + + This is called category in flash. Some sort of severity + +
+ +

+ +
+ An optional image: +
+

+
+ Timeout: +
+

+ +

+ +
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/src/cryptoadvance/specterext/devhelp/templates/devhelp/index.jinja b/src/cryptoadvance/specterext/devhelp/templates/devhelp/index.jinja index a63e38f010..cb0d9bff17 100644 --- a/src/cryptoadvance/specterext/devhelp/templates/devhelp/index.jinja +++ b/src/cryptoadvance/specterext/devhelp/templates/devhelp/index.jinja @@ -19,7 +19,8 @@

- Same for some macros. + Exploring/testing the Notification System +