mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
Compare commits
24 commits
v2.1.8-pre
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bcbae1a32 | ||
|
|
180801223d | ||
|
|
07eb1520dc | ||
|
|
f814ac47c5 | ||
|
|
69a43ef1ea | ||
|
|
6755046e4b | ||
|
|
8843e2f064 | ||
|
|
bf0780d6bc | ||
|
|
7b4d90f917 | ||
|
|
cf491ee5b9 | ||
|
|
9f890a90f8 | ||
|
|
1a3ecf07fb | ||
|
|
426ed39aac | ||
|
|
693338d3af | ||
|
|
3d554fbc2e | ||
|
|
48fea77b8a | ||
|
|
0b027f49fd | ||
|
|
fe232048d4 | ||
|
|
54b3616b40 | ||
|
|
44c5fdeaad | ||
|
|
febcf619d9 | ||
|
|
314a8579a0 | ||
|
|
a1a82082dd | ||
|
|
64139ebdb7 |
50 changed files with 3331 additions and 8295 deletions
25
.github/workflows/test.yml
vendored
25
.github/workflows/test.yml
vendored
|
|
@ -196,6 +196,31 @@ jobs:
|
|||
pip3 install -r requirements.txt --require-hashes
|
||||
pip3 install -e ".[test]"
|
||||
|
||||
- name: Dev-server smoketest (from the repo-root)
|
||||
# No job used to start the dev-server from the repo-root the way
|
||||
# docs/development.md describes it. That's why #2526 (renaming the
|
||||
# project to cryptoadvance_specter) could break it unnoticed.
|
||||
run: |
|
||||
source ./.env/bin/activate
|
||||
export SPECTER_DATA_FOLDER=$(mktemp -d)
|
||||
# --debug enables the werkzeug-reloader which forks a child-process, so
|
||||
# start a new process-group we can kill as a whole further down
|
||||
setsid python3 -m cryptoadvance.specter server --config DevelopmentConfig --debug > specterd.log 2>&1 &
|
||||
specterd_pid=$!
|
||||
started=""
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf http://127.0.0.1:25441/ > /dev/null; then started="yes"; break; fi
|
||||
sleep 2
|
||||
done
|
||||
kill -- -$specterd_pid || true
|
||||
# the next step needs port 25441, so make sure it's free again
|
||||
for i in $(seq 1 10); do
|
||||
curl -sf http://127.0.0.1:25441/ > /dev/null || break
|
||||
sleep 1
|
||||
done
|
||||
cat specterd.log
|
||||
if [ -z "$started" ]; then echo "The dev-server did not come up!"; exit 1; fi
|
||||
|
||||
- name: Extension smoketest
|
||||
run: |
|
||||
git config --global user.name "CI CD"
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -59,3 +59,7 @@ _bmad-output/
|
|||
|
||||
# Claude Code local workspace
|
||||
.claude/
|
||||
|
||||
# Local agent workspaces
|
||||
/.agents/
|
||||
/.history/
|
||||
|
|
|
|||
40
cypress.config.js
Normal file
40
cypress.config.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
const { defineConfig } = require('cypress')
|
||||
const setupNodeEvents = require('./cypress/plugins')
|
||||
|
||||
const specPattern = [
|
||||
'cypress/integration/spec_empty_specter_home.js',
|
||||
'cypress/integration/spec_connections.js',
|
||||
'cypress/integration/spec_devices.js',
|
||||
'cypress/integration/spec_ghost_machine.js',
|
||||
'cypress/integration/spec_fees.js',
|
||||
'cypress/integration/spec_rescan.js',
|
||||
'cypress/integration/spec_qr_signing.js',
|
||||
'cypress/integration/spec_labeling.js',
|
||||
'cypress/integration/spec_balances_amounts.js',
|
||||
'cypress/integration/spec_wallet_send.js',
|
||||
'cypress/integration/spec_wallet_utxo.js',
|
||||
'cypress/integration/spec_plugins.js',
|
||||
]
|
||||
|
||||
module.exports = defineConfig({
|
||||
e2e: {
|
||||
baseUrl: 'http://localhost:25444',
|
||||
specPattern,
|
||||
supportFile: 'cypress/support/index.js',
|
||||
testIsolation: false,
|
||||
setupNodeEvents,
|
||||
},
|
||||
env: {
|
||||
broadcast_timeout: 8000,
|
||||
},
|
||||
includeShadowDom: false,
|
||||
reporter: 'junit',
|
||||
reporterOptions: {
|
||||
mochaFile: 'cypresstest-output.xml',
|
||||
toConsole: true,
|
||||
},
|
||||
retries: {
|
||||
runMode: 1,
|
||||
openMode: 0,
|
||||
},
|
||||
})
|
||||
|
|
@ -29,8 +29,15 @@ describe('Completely empty specter-home', () => {
|
|||
})
|
||||
|
||||
it('Login with password and deactivate password protection again', () => {
|
||||
cy.get('[data-cy="admin-password"]').type('satoshi')
|
||||
cy.get('[data-cy="login-btn"]').click()
|
||||
cy.get('body').then(($body) => {
|
||||
// Cypress 13 keeps browser context when testIsolation is disabled for this
|
||||
// stateful suite. If the session cookie survived from the previous test,
|
||||
// we are already logged in and can go straight to settings.
|
||||
if ($body.find('[data-cy="admin-password"]').length) {
|
||||
cy.get('[data-cy="admin-password"]').type('satoshi')
|
||||
cy.get('[data-cy="login-btn"]').click()
|
||||
}
|
||||
})
|
||||
cy.get('[data-cy="settings-btn"]').click()
|
||||
cy.contains('Authentication').click()
|
||||
cy.get('[data-cy="authentication-selection"]').select('None')
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ describe('Test the fee UI', () => {
|
|||
cy.intercept('POST', '/wallets/wallet/ghost_wallet/estimate_fee', {fee: 0.00005944})
|
||||
cy.get('[data-cy="show-estimated-fee-btn"]').click()
|
||||
cy.contains('You provided no address')
|
||||
cy.get('#recipient_0').find('#address').invoke('val', "bcrt1qvtdx75y4554ngrq6aff3xdqnvjhmct5wck95qs")
|
||||
cy.get('#recipient_0').find('#address').invoke('val', "bcrt1qvtdx75y4554ngrq6aff3xdqnvjhmct5wck95qs").trigger('input')
|
||||
cy.get('[data-cy="sats-checkbox"]').click()
|
||||
cy.get('#recipient_0').find('[data-cy="amount-input"]').type(50000, { force: true })
|
||||
cy.get('[data-cy="show-estimated-fee-btn"]').click()
|
||||
|
|
|
|||
|
|
@ -5,10 +5,8 @@ describe('Test the labeling of addresses and transactions', () => {
|
|||
cy.visit('/')
|
||||
})
|
||||
|
||||
// Keeps the session cookie alive, Cypress by default clears all cookies before each test
|
||||
beforeEach(() => {
|
||||
cy.viewport('macbook-13')
|
||||
Cypress.Cookies.preserveOnce('session')
|
||||
})
|
||||
|
||||
it('Labeling an address on the address overview', () => {
|
||||
|
|
|
|||
|
|
@ -26,9 +26,16 @@ describe('Test plugins', () => {
|
|||
})
|
||||
|
||||
it('Deactivate password protection', () => {
|
||||
// This flow only works if we don't keep the session alive! So, no Cypress.Cookies.preserveOnce('session') in beforeEach().
|
||||
cy.get('#password').type("mySecretPassword")
|
||||
cy.get('#login-btn').click()
|
||||
cy.get('body').then(($body) => {
|
||||
// Cypress 13 keeps browser context when testIsolation is disabled for
|
||||
// this stateful suite. If the session cookie survived from the
|
||||
// previous test, we are already logged in and can go straight to
|
||||
// settings; otherwise log in first.
|
||||
if ($body.find('#password').length) {
|
||||
cy.get('#password').type("mySecretPassword")
|
||||
cy.get('#login-btn').click()
|
||||
}
|
||||
})
|
||||
cy.get('[data-cy="settings-btn"]').click()
|
||||
cy.get('[href="/settings/auth"]').click()
|
||||
cy.get('select').select("none")
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import './commands'
|
|||
// Set the view port for each test
|
||||
beforeEach(() => {
|
||||
cy.viewport('macbook-13')
|
||||
cy.session('preserveSession', () => {}) // Keeps the session cookie alive, Cypress by default clears all cookies before each test
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
- [Set up virtualenv](#set-up-virtualenv)
|
||||
- [If `pip install` fails on `cryptography==3.4.x`](#if-pip-install-fails-on-cryptography34x)
|
||||
- [How to run the tests](#how-to-run-the-tests)
|
||||
- [Hardware-attended Jade tests](#hardware-attended-jade-tests)
|
||||
- [Code-Style](#code-style)
|
||||
- [Developing on tests](#developing-on-tests)
|
||||
- [bitcoin-specific stuff](#bitcoin-specific-stuff)
|
||||
|
|
@ -209,6 +210,36 @@ Print the logging output live to the terminal:
|
|||
pytest --capture=no --log-cli-level=DEBUG
|
||||
```
|
||||
|
||||
### Hardware-attended Jade tests
|
||||
|
||||
`tests/test_jade_hardware.py` exercises Specter's HWI integration end-to-end against a physical Blockstream Jade. It is gated by `--run-jade-hardware` and skipped by default, so GitHub Actions ignore it without any workflow change.
|
||||
|
||||
Run with `-s` so operator prompts reach the terminal:
|
||||
```
|
||||
pytest --run-jade-hardware tests/test_jade_hardware.py -s
|
||||
```
|
||||
|
||||
Three tests, increasing operator effort:
|
||||
|
||||
| Test | What it does | Operator action |
|
||||
|---|---|---|
|
||||
| `test_jade_enumerate_via_specter` | `HWIBridge.enumerate()` finds the Jade and returns a fingerprint | Connect Jade, unlock |
|
||||
| `test_jade_extract_xpub_via_specter` | Pulls xpub at `m/84h/0h/0h` (mainnet) | Confirm xpub export on device |
|
||||
| `test_jade_sign_psbt_via_specter` | Signs a canned testnet PSBT through Specter's sign path | Boot Jade in Temporary Signer mode, scan SeedQR, confirm tx |
|
||||
|
||||
The signing test uses the public **BIP-39 abandon vector** (`abandon abandon ... about`) so the PSBT fixture matches anyone's Jade once they load that seed. Setup procedure:
|
||||
|
||||
1. Power-cycle the Jade so it shows the boot menu.
|
||||
2. Choose **Temporary Signer** -> **Scan SeedQR**.
|
||||
3. Display `tests/fixtures/jade_seedqr_abandon.png` (or `cat tests/fixtures/jade_seedqr_abandon.txt` for the ASCII version) and scan it with the Jade camera.
|
||||
4. When Jade asks for the network, select **TESTNET**.
|
||||
5. Press Enter at the test prompt.
|
||||
6. Confirm the transaction on the Jade screen when it pops up (~99,500 sats to a testnet bech32 output, ~99,500 change auto-validated, 1,000 fee).
|
||||
|
||||
Temporary Signer state is held in RAM only and wiped on power-cycle/USB-unplug — your real seed is not affected. Expected master fingerprint for the abandon vector is `73c5da0a`; the test fails fast with a clear hint if the loaded seed is wrong.
|
||||
|
||||
The fixture PSBT (`tests/fixtures/jade_hardware.psbt`) was generated with embit against m/84'/1'/0'/0/0 of the abandon vector, including a synthetic `non_witness_utxo` so Jade can verify the input amount per the SegWit fee-spoof mitigation.
|
||||
|
||||
Get the log-output of bitcoind side by side with the test-output. For sure you will only see the logs if the test fails.
|
||||
```
|
||||
pytest --bitcoind-log-stdout
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
- [*Can this also work with external nodes like Casa, MyNode, and Raspilitz?*](#can-this-also-work-with-external-nodes-like-casa-mynode-and-raspilitz)
|
||||
- [*Can I use Tor?*](#can-i-use-tor)
|
||||
- [I forgot my password, how can I reset it?](#i-forgot-my-password-how-can-i-reset-it)
|
||||
- [Does the Specter password encrypt my wallet data files?](#does-the-specter-password-encrypt-my-wallet-data-files)
|
||||
- [BACKING UP FUNDS](#backing-up-funds)
|
||||
- [*If something happens to the `~/.specter` folder, is it still possible to **restore** access to multisigs created there (assuming there is no backup of the `~/.specter` folder)?*](#if-something-happens-to-the-specter-folder-is-it-still-possible-to-restore-access-to-multisigs-created-there-assuming-there-is-no-backup-of-the-specter-folder)
|
||||
- [*To recover a multisig that was built on specter (eg: 2 of 3 with ColdCard), is having the seeds of all 3 signing wallets sufficient or do we need to backup more info?*](#to-recover-a-multisig-that-was-built-on-specter-eg-2-of-3-with-coldcard-is-having-the-seeds-of-all-3-signing-wallets-sufficient-or-do-we-need-to-backup-more-info)
|
||||
|
|
@ -241,6 +242,12 @@ Depending on "what's written in `somethingInHere`:
|
|||
* If it's `usernamepassword`, you won't be able to recover the password but you can deactivate it by setting it to `none` and restart Specter.
|
||||
* If it's `none` (or you just set it to `none`) you can login without any password. So hurry up with setting it again within specter.
|
||||
|
||||
### Does the Specter password encrypt my wallet data files?
|
||||
|
||||
No. Specter authentication protects access to the app/session, but it does not encrypt the whole Specter data folder at rest. The data folder contains wallet metadata files such as xpubs, addresses, wallet names, labels, and transaction metadata that may be readable by anyone or any process with filesystem access to that folder.
|
||||
|
||||
If this is in your threat model, use OS-level disk encryption (for example FileVault on macOS), make sure backups and sync tools that include the Specter data folder are encrypted and access-controlled, and consider running Specter from a data folder located on an encrypted volume/container via `--specter-data-folder`.
|
||||
|
||||
## BACKING UP FUNDS
|
||||
|
||||
### *If something happens to the `~/.specter` folder, is it still possible to **restore** access to multisigs created there (assuming there is no backup of the `~/.specter` folder)?*
|
||||
|
|
|
|||
7696
package-lock.json
generated
7696
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -13,9 +13,9 @@
|
|||
"wait-on": "^9.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cypress": "^9.7.0",
|
||||
"cypress": "^13.17.0",
|
||||
"cypress-wait-until": "^1.7.1",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp": "^5.0.1",
|
||||
"tailwindcss": "^3.2.4"
|
||||
},
|
||||
"overrides": {
|
||||
|
|
|
|||
72
pyinstaller/electron/package-lock.json
generated
72
pyinstaller/electron/package-lock.json
generated
|
|
@ -19,7 +19,7 @@
|
|||
"winston": "^3.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^39.8.5",
|
||||
"electron": "^39.8.10",
|
||||
"electron-builder": "^26.8.1"
|
||||
}
|
||||
},
|
||||
|
|
@ -2010,9 +2010,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "39.8.5",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-39.8.5.tgz",
|
||||
"integrity": "sha512-q6+LiQIcTadSyvtPgLDQkCtVA9jQJXQVMrQcctfOJILh6OFMN+UJJLRkuUTy8CZDYeCIBn1ZycqsL1dAXugxZA==",
|
||||
"version": "39.8.10",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-39.8.10.tgz",
|
||||
"integrity": "sha512-zbYtGPYUI7PzqLAzkk21Rk6j67WN0hxn0Mq/njErZo1d0HSf33is4f8ICI5fMLy5vYe0JtCtM5sYunNOaochSQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
|
|
@ -3052,9 +3052,9 @@
|
|||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz",
|
||||
"integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
|
|
@ -3186,10 +3186,20 @@
|
|||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
|
|
@ -4580,9 +4590,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.13",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz",
|
||||
"integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==",
|
||||
"version": "7.5.22",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
|
||||
"integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
|
|
@ -4722,9 +4732,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
|
||||
"integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
|
|
@ -6605,9 +6615,9 @@
|
|||
}
|
||||
},
|
||||
"electron": {
|
||||
"version": "39.8.5",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-39.8.5.tgz",
|
||||
"integrity": "sha512-q6+LiQIcTadSyvtPgLDQkCtVA9jQJXQVMrQcctfOJILh6OFMN+UJJLRkuUTy8CZDYeCIBn1ZycqsL1dAXugxZA==",
|
||||
"version": "39.8.10",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-39.8.10.tgz",
|
||||
"integrity": "sha512-zbYtGPYUI7PzqLAzkk21Rk6j67WN0hxn0Mq/njErZo1d0HSf33is4f8ICI5fMLy5vYe0JtCtM5sYunNOaochSQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@electron/get": "^2.0.0",
|
||||
|
|
@ -7379,9 +7389,9 @@
|
|||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"ip-address": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz",
|
||||
"integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-arrayish": {
|
||||
|
|
@ -7466,9 +7476,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"argparse": "^2.0.1"
|
||||
|
|
@ -8516,9 +8526,9 @@
|
|||
}
|
||||
},
|
||||
"tar": {
|
||||
"version": "7.5.13",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz",
|
||||
"integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==",
|
||||
"version": "7.5.22",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
|
||||
"integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
|
|
@ -8635,9 +8645,9 @@
|
|||
}
|
||||
},
|
||||
"tmp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
|
||||
"integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
|
||||
"dev": true
|
||||
},
|
||||
"tmp-promise": {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"author": "Specter",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "^39.8.5",
|
||||
"electron": "^39.8.10",
|
||||
"electron-builder": "^26.8.1"
|
||||
},
|
||||
"build": {
|
||||
|
|
|
|||
|
|
@ -949,10 +949,10 @@ electron-publish@26.8.1:
|
|||
lazy-val "^1.0.5"
|
||||
mime "^2.5.2"
|
||||
|
||||
electron@^39.8.5:
|
||||
version "39.8.5"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-39.8.5.tgz#422d42318d993a77a960ea1a9b4bfb4822221388"
|
||||
integrity sha512-q6+LiQIcTadSyvtPgLDQkCtVA9jQJXQVMrQcctfOJILh6OFMN+UJJLRkuUTy8CZDYeCIBn1ZycqsL1dAXugxZA==
|
||||
electron@^39.8.10:
|
||||
version "39.8.10"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-39.8.10.tgz#998d9c5b9e7601debb70bd7355e8d4dfd6d70f43"
|
||||
integrity sha512-zbYtGPYUI7PzqLAzkk21Rk6j67WN0hxn0Mq/njErZo1d0HSf33is4f8ICI5fMLy5vYe0JtCtM5sYunNOaochSQ==
|
||||
dependencies:
|
||||
"@electron/get" "^2.0.0"
|
||||
"@types/node" "^22.7.7"
|
||||
|
|
@ -1465,9 +1465,9 @@ inherits@2, inherits@^2.0.3, inherits@^2.0.4:
|
|||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
ip-address@^10.0.1:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.1.0.tgz#d8dcffb34d0e02eb241427444a6e23f5b0595aa4"
|
||||
integrity sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==
|
||||
version "10.4.0"
|
||||
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.4.0.tgz#c5910bc541b6eae287765d1e4846be0308a05d93"
|
||||
integrity sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==
|
||||
|
||||
is-arrayish@^0.3.1:
|
||||
version "0.3.2"
|
||||
|
|
@ -1554,9 +1554,9 @@ jiti@^2.4.2:
|
|||
integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==
|
||||
|
||||
js-yaml@^4.1.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz"
|
||||
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.1.tgz#01216c001d67f48e2cd560d708c7af21090a3848"
|
||||
integrity sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
|
|
@ -2472,9 +2472,9 @@ supports-color@^7.1.0:
|
|||
has-flag "^4.0.0"
|
||||
|
||||
tar@^7.4.3, tar@^7.5.6, tar@^7.5.7:
|
||||
version "7.5.13"
|
||||
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.13.tgz#0d214ed56781a26edc313581c0e2d929ceeb866d"
|
||||
integrity sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==
|
||||
version "7.5.22"
|
||||
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.22.tgz#a696f998136e71487dc3f869a85bba2c67971ba9"
|
||||
integrity sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==
|
||||
dependencies:
|
||||
"@isaacs/fs-minipass" "^4.0.0"
|
||||
chownr "^3.0.0"
|
||||
|
|
@ -2532,9 +2532,9 @@ tmp-promise@^3.0.2:
|
|||
tmp "^0.2.0"
|
||||
|
||||
tmp@^0.2.0:
|
||||
version "0.2.5"
|
||||
resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz"
|
||||
integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==
|
||||
version "0.2.7"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.7.tgz#26f4db11d1601ce8012dcb8a798ece1c06a99059"
|
||||
integrity sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==
|
||||
|
||||
tough-cookie@~2.5.0:
|
||||
version "2.5.0"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
pyinstaller==6.11.1
|
||||
pyinstaller-hooks-contrib>=2024.0
|
||||
setuptools>=78.1.1
|
||||
backports.tarfile==1.2.0
|
||||
|
||||
# Platform helpers
|
||||
altgraph>=0.17
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ readme = "README.md"
|
|||
license = {file = "LICENSE"}
|
||||
|
||||
|
||||
requires-python = ">=3.7,<4.0"
|
||||
requires-python = ">=3.9,<3.13"
|
||||
|
||||
dynamic = ["dependencies", "version"]
|
||||
|
||||
|
|
@ -52,7 +52,8 @@ markers = [
|
|||
"slow: mark test as slow.",
|
||||
"elm: mark test as elementsd dependent",
|
||||
"bottleneck: mark a test as so ressource intensive that it can create a bottleneck where the test just fails due to a lack of ressources",
|
||||
"threading: test needs threading to work"
|
||||
"threading: test needs threading to work",
|
||||
"jade_hardware: requires a real Jade attached and an operator; opt-in only via --run-jade-hardware"
|
||||
]
|
||||
|
||||
filterwarnings = [
|
||||
|
|
@ -75,17 +76,17 @@ dependencies = {file = ["requirements.in"]}
|
|||
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
"black==22.3.0",
|
||||
"black==26.3.1",
|
||||
"pre-commit==2.13.0",
|
||||
"pip-tools==6.13",
|
||||
"pytest==7.1.2",
|
||||
"pytest==9.0.3",
|
||||
"PySocks==1.7.1",
|
||||
"pytest-cov==2.10.1",
|
||||
"mock==4.0.2",
|
||||
# requirements for stuff in ./utils
|
||||
"requests==2.31.0",
|
||||
"requests==2.33.0",
|
||||
]
|
||||
|
||||
gendownloadpage = [
|
||||
"markdown==3.3.7"
|
||||
"markdown==3.8.1"
|
||||
]
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
certifi==2024.6.2
|
||||
certifi==2024.7.4
|
||||
Click==8.1.7
|
||||
Flask==2.2.5
|
||||
# Flask 2.2.x test_client uses werkzeug.__version__, removed in Werkzeug 3.1.
|
||||
Werkzeug==3.0.6
|
||||
Flask-Babel==3.1.0
|
||||
Flask-Cors==6.0.0
|
||||
Flask-Login==0.6.3
|
||||
Flask-RESTful==0.3.10
|
||||
Flask-HTTPAuth==4.8.0
|
||||
hwi==2.4.0
|
||||
python-dotenv==0.21.1
|
||||
requests==2.31.0
|
||||
Flask-HTTPAuth==4.8.1
|
||||
hwi==3.1.0
|
||||
python-dotenv==1.2.2
|
||||
requests==2.33.0
|
||||
pysocks==1.7.1
|
||||
six==1.16.0
|
||||
stem==1.8.0
|
||||
|
|
@ -24,7 +26,7 @@ Flask-APScheduler==1.12.4
|
|||
gunicorn==23.0.0
|
||||
simple-websocket==0.8.1
|
||||
protobuf==4.23.3
|
||||
PyJWT==2.12.0
|
||||
PyJWT==2.13.0
|
||||
pytimeparse==1.1.8
|
||||
psycopg2-binary==2.9.5
|
||||
aioitertools==0.11.0
|
||||
|
|
@ -33,9 +35,9 @@ aioitertools==0.11.0
|
|||
cryptoadvance-liquidissuer==0.2.4
|
||||
specterext-exfund==0.1.7
|
||||
specterext-faucet==0.1.2
|
||||
cryptoadvance.spectrum==0.7.0
|
||||
cryptoadvance.spectrum==0.8.0
|
||||
specterext-stacktrack==0.3.0
|
||||
specterext-timelockrecovery==0.2.1
|
||||
specterext-timelockrecovery==0.2.3
|
||||
|
||||
# workarounds
|
||||
|
||||
|
|
|
|||
|
|
@ -68,9 +68,9 @@ cbor2==5.9.0 \
|
|||
# via
|
||||
# -r requirements.in
|
||||
# hwi
|
||||
certifi==2024.6.2 \
|
||||
--hash=sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516 \
|
||||
--hash=sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56
|
||||
certifi==2024.7.4 \
|
||||
--hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \
|
||||
--hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90
|
||||
# via
|
||||
# -r requirements.in
|
||||
# requests
|
||||
|
|
@ -174,9 +174,9 @@ cryptoadvance-liquidissuer==0.2.4 \
|
|||
--hash=sha256:5a2c531801854c5a4a46daf184877e22f731cdb42d2cfb840785bda7371ba6fb \
|
||||
--hash=sha256:9e468f3e35ecc566b3f74a2263677cf26632548abb194521dba15ad37acd1e9b
|
||||
# via -r requirements.in
|
||||
cryptoadvance-spectrum==0.7.0 \
|
||||
--hash=sha256:40b31d38ad40c85438bc44d9243edb9ddbaf331c4468a48ebc0f40a109b43a62 \
|
||||
--hash=sha256:6cd8858fa07668536345ecab08b4143e28055d7524c9b27b8c885c5b98730879
|
||||
cryptoadvance-spectrum==0.8.0 \
|
||||
--hash=sha256:cc91eef05f6417898e735b793b91135ddd989fe223ff51aeaa4bc278eb15c3ed \
|
||||
--hash=sha256:d3a53b21ca13d8e6a014ff2c0b0e68f8d4eef2c1efbe6383ae018c15409c67c1
|
||||
# via -r requirements.in
|
||||
cryptography==46.0.6 \
|
||||
--hash=sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70 \
|
||||
|
|
@ -267,9 +267,9 @@ flask-cors==6.0.0 \
|
|||
--hash=sha256:4592c1570246bf7beee96b74bc0adbbfcb1b0318f6ba05c412e8909eceec3393 \
|
||||
--hash=sha256:6332073356452343a8ccddbfec7befdc3fdd040141fe776ec9b94c262f058657
|
||||
# via -r requirements.in
|
||||
flask-httpauth==4.8.0 \
|
||||
--hash=sha256:66568a05bc73942c65f1e2201ae746295816dc009edd84b482c44c758d75097a \
|
||||
--hash=sha256:a58fedd09989b9975448eef04806b096a3964a7feeebc0a78831ff55685b62b0
|
||||
flask-httpauth==4.8.1 \
|
||||
--hash=sha256:0080393d70e12327781f7509115175ec5e47209816489a620d4fd39e20cea2e8 \
|
||||
--hash=sha256:88499b22f1353893743c3cd68f2ca561c4ad9ef75cd6bcc7f621161cd0e80744
|
||||
# via -r requirements.in
|
||||
flask-login==0.6.3 \
|
||||
--hash=sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333 \
|
||||
|
|
@ -352,7 +352,9 @@ greenlet==2.0.2 \
|
|||
--hash=sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19 \
|
||||
--hash=sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1 \
|
||||
--hash=sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526
|
||||
# via -r requirements.in
|
||||
# via
|
||||
# -r requirements.in
|
||||
# sqlalchemy
|
||||
gunicorn==23.0.0 \
|
||||
--hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \
|
||||
--hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec
|
||||
|
|
@ -432,13 +434,13 @@ hidapi==0.14.0 \
|
|||
--hash=sha256:fb4e94e45f6dddb20d59501187721e5d3b02e6cc8a59d261dd5cac739008582a \
|
||||
--hash=sha256:fc9ec2321bf3b0b4953910aa87c0c8ab5f93b1f113a9d3d4f18845ce54708d13
|
||||
# via hwi
|
||||
hwi==2.4.0 \
|
||||
--hash=sha256:3eaa7593f1ab360569eacdd9507dab75532bb58e8cd991d8ad72f5c4fcb67997 \
|
||||
--hash=sha256:7cb7ef2a4db4bc434815374d9bad43c6425491f77828314a2d2898d3e86d3f04
|
||||
hwi==3.1.0 \
|
||||
--hash=sha256:21ba92bb06e2f805e2806c686f2c50d02db6826a363b01e44052415755504d6f \
|
||||
--hash=sha256:42e875cbb616a91638fb90679cad93edb5075bf375e92fc1709be9b2a3dfd59c
|
||||
# via -r requirements.in
|
||||
idna==3.7 \
|
||||
--hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
|
||||
--hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
|
||||
idna==3.15 \
|
||||
--hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \
|
||||
--hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc
|
||||
# via requests
|
||||
itsdangerous==2.2.0 \
|
||||
--hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \
|
||||
|
|
@ -446,9 +448,9 @@ itsdangerous==2.2.0 \
|
|||
# via
|
||||
# flask
|
||||
# flask-wtf
|
||||
jinja2==3.1.4 \
|
||||
--hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \
|
||||
--hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d
|
||||
jinja2==3.1.6 \
|
||||
--hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
|
||||
--hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
|
||||
# via
|
||||
# flask
|
||||
# flask-babel
|
||||
|
|
@ -742,17 +744,17 @@ psycopg2-binary==2.9.5 \
|
|||
pyaes==1.6.1 \
|
||||
--hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
|
||||
# via hwi
|
||||
pyasn1==0.6.3 \
|
||||
--hash=sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf \
|
||||
--hash=sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde
|
||||
pyasn1==0.6.4 \
|
||||
--hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \
|
||||
--hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b
|
||||
# via pgpy
|
||||
pycparser==2.22 \
|
||||
--hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
|
||||
--hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
|
||||
# via cffi
|
||||
pyjwt==2.12.0 \
|
||||
--hash=sha256:2f62390b667cd8257de560b850bb5a883102a388829274147f1d724453f8fb02 \
|
||||
--hash=sha256:9bb459d1bdd0387967d287f5656bf7ec2b9a26645d1961628cda1764e087fd6e
|
||||
pyjwt==2.13.0 \
|
||||
--hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \
|
||||
--hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728
|
||||
# via -r requirements.in
|
||||
pyopenssl==26.0.0 \
|
||||
--hash=sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81 \
|
||||
|
|
@ -775,9 +777,9 @@ python-dateutil==2.9.0.post0 \
|
|||
# via
|
||||
# flask-apscheduler
|
||||
# pandas
|
||||
python-dotenv==0.21.1 \
|
||||
--hash=sha256:1c93de8f636cde3ce377292818d0e440b6e45a82f215c3744979151fa8151c49 \
|
||||
--hash=sha256:41e12e0318bebc859fcc4d97d4db8d20ad21721a6aa5047dd59f090391cb549a
|
||||
python-dotenv==1.2.2 \
|
||||
--hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \
|
||||
--hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3
|
||||
# via -r requirements.in
|
||||
pytimeparse==1.1.8 \
|
||||
--hash=sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd \
|
||||
|
|
@ -791,9 +793,9 @@ pytz==2024.1 \
|
|||
# flask-babel
|
||||
# flask-restful
|
||||
# pandas
|
||||
requests==2.31.0 \
|
||||
--hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
|
||||
--hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
|
||||
requests==2.33.0 \
|
||||
--hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \
|
||||
--hash=sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652
|
||||
# via
|
||||
# -r requirements.in
|
||||
# cryptoadvance-spectrum
|
||||
|
|
@ -824,9 +826,9 @@ specterext-stacktrack==0.3.0 \
|
|||
--hash=sha256:14f96f1f552f57ba017b8bc642f07343edbb1abafe09e03bbaae179d78d7ce23 \
|
||||
--hash=sha256:9e2946185730aab377951e83a27d8791a34e0f031e44f15991212b6b85722ca0
|
||||
# via -r requirements.in
|
||||
specterext-timelockrecovery==0.2.1 \
|
||||
--hash=sha256:2fc2a3eec045377b54926ff41dab9c5f5a6f46074150aec1ff67cb1456fef911 \
|
||||
--hash=sha256:35e181971b5b69ca6706496169c4581f2fa20706c88ba24760e56e153ec51939
|
||||
specterext-timelockrecovery==0.2.3 \
|
||||
--hash=sha256:5df3deb6245a22d48f75bbb62fd87dc141a9f60407011ce9fbb0a7bcc40fe4cc \
|
||||
--hash=sha256:a0b22c4e010061055da4ea6ea4e08bbc38d1f7c2ec58327970caa0ce987cba95
|
||||
# via -r requirements.in
|
||||
sqlalchemy==1.4.52 \
|
||||
--hash=sha256:1296f2cdd6db09b98ceb3c93025f0da4835303b8ac46c15c2136e27ee4d18d94 \
|
||||
|
|
@ -891,6 +893,7 @@ typing-extensions==4.15.0 \
|
|||
# via
|
||||
# cryptography
|
||||
# hwi
|
||||
# pyjwt
|
||||
# pyopenssl
|
||||
tzdata==2024.1 \
|
||||
--hash=sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd \
|
||||
|
|
@ -900,14 +903,15 @@ tzlocal==5.2 \
|
|||
--hash=sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8 \
|
||||
--hash=sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e
|
||||
# via apscheduler
|
||||
urllib3==2.6.3 \
|
||||
--hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \
|
||||
--hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4
|
||||
urllib3==2.7.0 \
|
||||
--hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \
|
||||
--hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
|
||||
# via requests
|
||||
werkzeug==3.0.3 \
|
||||
--hash=sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18 \
|
||||
--hash=sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8
|
||||
werkzeug==3.0.6 \
|
||||
--hash=sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17 \
|
||||
--hash=sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d
|
||||
# via
|
||||
# -r requirements.in
|
||||
# flask
|
||||
# flask-cors
|
||||
# flask-login
|
||||
|
|
@ -921,5 +925,6 @@ wtforms==3.1.2 \
|
|||
# via flask-wtf
|
||||
|
||||
# WARNING: The following packages were not pinned, but pip requires them to be
|
||||
# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag.
|
||||
# pinned when the requirements file includes hashes and the requirement is not
|
||||
# satisfied by a package already installed. Consider using the --allow-unsafe flag.
|
||||
# setuptools
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
""" Security Related things for the REST-API """
|
||||
"""Security Related things for the REST-API"""
|
||||
|
||||
import logging, jwt
|
||||
from functools import wraps
|
||||
|
||||
|
|
@ -14,6 +15,18 @@ from flask import current_app as app
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@auth.error_handler
|
||||
@token_auth.error_handler
|
||||
def auth_error(status):
|
||||
"""Return REST API authentication failures as JSON.
|
||||
|
||||
Flask-HTTPAuth 4.8.1 changed the default unauthorized response body to
|
||||
plain text ("Unauthorized Access"). The REST API has historically returned
|
||||
Flask-RESTful JSON error objects, and clients/tests rely on that shape.
|
||||
"""
|
||||
return abort(status)
|
||||
|
||||
|
||||
@auth.verify_password
|
||||
def verify_password(username, password):
|
||||
"""Validate user passwords and store user in the 'g' object"""
|
||||
|
|
@ -40,11 +53,21 @@ def verify_token(jwt_token):
|
|||
return abort(401)
|
||||
try:
|
||||
payload = jwt.decode(jwt_token, app.config["SECRET_KEY"], algorithms=["HS256"])
|
||||
username = payload["username"]
|
||||
the_user = app.specter.user_manager.get_user_by_username(username)
|
||||
if not the_user:
|
||||
username = payload.get("username")
|
||||
jwt_token_id = payload.get("jwt_token_id")
|
||||
if (
|
||||
not isinstance(username, str)
|
||||
or not username
|
||||
or not isinstance(jwt_token_id, str)
|
||||
or not jwt_token_id
|
||||
):
|
||||
return abort(401)
|
||||
g.user = app.specter.user_manager.get_user_by_username(username)
|
||||
the_user = app.specter.user_manager.get_user_by_username(username)
|
||||
if not the_user or not the_user.verify_jwt_token_id_and_jwt_token(
|
||||
jwt_token_id, jwt_token
|
||||
):
|
||||
return abort(401)
|
||||
g.user = the_user
|
||||
logger.info({"payload": payload})
|
||||
logger.info(f"Rest-Request for user {username} PASSED JWT-test")
|
||||
return username
|
||||
|
|
|
|||
|
|
@ -179,7 +179,14 @@ def server(
|
|||
print(
|
||||
" * Running in HWI Bridge mode.\n"
|
||||
" * You can configure access to the API "
|
||||
"at: %s://%s:%d/hwi/settings" % ("http", host, app.config["PORT"])
|
||||
"at: %s://%s:%d%s%s/hwi/settings"
|
||||
% (
|
||||
"http",
|
||||
host,
|
||||
app.config["PORT"],
|
||||
app.config["APP_URL_PREFIX"],
|
||||
app.config["SPECTER_URL_PREFIX"],
|
||||
)
|
||||
)
|
||||
|
||||
# debug is false by default
|
||||
|
|
|
|||
|
|
@ -1,7 +1,16 @@
|
|||
import json, os, random, requests
|
||||
from flask import Blueprint, Flask, jsonify, url_for, redirect, render_template, request
|
||||
from flask import (
|
||||
Blueprint,
|
||||
Flask,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
url_for,
|
||||
)
|
||||
from .server_endpoints import flash
|
||||
from flask import current_app as app
|
||||
from flask_login import current_user, login_required
|
||||
from flask_cors import CORS
|
||||
from .hwi_rpc import HWIBridge
|
||||
from .helpers import deep_update, hwi_get_config, save_hwi_bridge_config
|
||||
|
|
@ -10,13 +19,14 @@ import logging
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
hwi_server = Blueprint("hwi_server", __name__)
|
||||
hwi_server_settings = Blueprint("hwi_server_settings", __name__)
|
||||
CORS(hwi_server)
|
||||
rand = random.randint(0, int(1e32)) # to force style refresh
|
||||
|
||||
|
||||
@hwi_server.route("/", methods=["GET"])
|
||||
def index():
|
||||
return redirect(url_for("hwi_server.hwi_bridge_settings"))
|
||||
return redirect(url_for("hwi_server_settings.hwi_bridge_settings"))
|
||||
|
||||
|
||||
@hwi_server.route("/api/", methods=["POST"])
|
||||
|
|
@ -100,8 +110,15 @@ def api():
|
|||
return jsonify(app.specter.hwi.jsonrpc(data))
|
||||
|
||||
|
||||
@hwi_server.route("/settings/", methods=["GET", "POST"])
|
||||
@hwi_server_settings.route("/settings/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def hwi_bridge_settings():
|
||||
if app.config.get("LOGIN_DISABLED") and (
|
||||
not current_user.is_authenticated or not current_user.is_admin
|
||||
):
|
||||
app.login("admin")
|
||||
if not current_user.is_admin:
|
||||
return "Forbidden", 403
|
||||
config = hwi_get_config(app.specter)
|
||||
if request.method == "POST":
|
||||
action = request.form["action"]
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from werkzeug.wrappers import Response
|
|||
from cryptoadvance.specter.hwi_rpc import HWIBridge
|
||||
|
||||
from .htmlsafebabel import HTMLSafeBabel
|
||||
from .hwi_server import hwi_server
|
||||
from .hwi_server import hwi_server, hwi_server_settings
|
||||
from .services.callbacks import after_serverpy_init_app, specter_added_to_flask_app
|
||||
from .specter import Specter
|
||||
from .util.specter_migrator import SpecterMigrator
|
||||
|
|
@ -229,8 +229,16 @@ def init_app(app: SpecterFlask, hwibridge=False, specter=None):
|
|||
app.logger.info("Login enabled")
|
||||
app.config["LOGIN_DISABLED"] = False
|
||||
app.logger.info("Initializing Controller ...")
|
||||
hwi_settings_prefix = f"{app.config['SPECTER_URL_PREFIX']}/hwi"
|
||||
app.register_blueprint(hwi_server_settings, url_prefix=hwi_settings_prefix)
|
||||
app.register_blueprint(hwi_server, url_prefix="/hwi")
|
||||
csrf.exempt(hwi_server)
|
||||
csrf.exempt(app.view_functions["hwi_server.api"])
|
||||
if hwi_settings_prefix != "/hwi":
|
||||
app.add_url_rule(
|
||||
"/hwi/settings/",
|
||||
"hwi_server.hwi_bridge_settings",
|
||||
lambda: redirect(url_for("hwi_server_settings.hwi_bridge_settings")),
|
||||
)
|
||||
if not hwibridge:
|
||||
with app.app_context():
|
||||
from cryptoadvance.specter.server_endpoints import controller
|
||||
|
|
@ -258,10 +266,17 @@ def init_app(app: SpecterFlask, hwibridge=False, specter=None):
|
|||
importlib.reload(controller)
|
||||
importlib.reload(serviceController)
|
||||
else:
|
||||
with app.app_context():
|
||||
from cryptoadvance.specter.server_endpoints.auth import auth_endpoint
|
||||
|
||||
app.register_blueprint(
|
||||
auth_endpoint,
|
||||
url_prefix=f"{app.config['SPECTER_URL_PREFIX']}/auth",
|
||||
)
|
||||
|
||||
@app.route("/", methods=["GET"])
|
||||
def index():
|
||||
return redirect(url_for("hwi_server.hwi_bridge_settings"))
|
||||
return redirect(url_for("hwi_server_settings.hwi_bridge_settings"))
|
||||
|
||||
if app.config["SPECTER_API_ACTIVE"]:
|
||||
app.logger.info("Initializing REST ...")
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ class ExtGen:
|
|||
shutil.copy(sourcepath, targetpath)
|
||||
print(f" --> Created {targetpath} (copied)")
|
||||
else:
|
||||
r = requests.get(self.env.loader.url_for_template(sourcepath))
|
||||
r = requests.get(self.env.loader.url_for_template(sourcepath), timeout=30)
|
||||
open(targetpath, "wb").write(r.content)
|
||||
print(f" --> Created {targetpath} (via Github)")
|
||||
|
||||
|
|
@ -233,7 +233,7 @@ class GithubUrlLoader(BaseLoader):
|
|||
def get_source(self, environment, template):
|
||||
url = self.url_for_template(template)
|
||||
for attempt in range(3):
|
||||
r = requests.get(url)
|
||||
r = requests.get(url, timeout=30)
|
||||
if r.status_code == 200:
|
||||
return r.text, url, None
|
||||
if r.status_code == 429 and attempt < 2:
|
||||
|
|
|
|||
|
|
@ -1100,6 +1100,10 @@ input[type="number"]::-webkit-outer-spin-button,
|
|||
z-index: 50;
|
||||
}
|
||||
|
||||
.z-20 {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.z-30 {
|
||||
z-index: 30;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,16 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="coldcard-warning" class="flex p-4 mt-3 mb-4 bg-orange-100 text-orange-700 rounded-lg {% if device_class.device_type != 'coldcard' %}hidden{% endif %}" role="alert">
|
||||
<img class="flex-shrink-0 w-6 h-6 mr-3" src="{{ url_for('static', filename='img/warning_sign.svg') }}" aria-hidden="true">
|
||||
<span class="sr-only">{{ _("Security warning") }}</span>
|
||||
<div>
|
||||
<span class="font-medium">{{ _("Security warning: ColdCard seed entropy advisory") }}</span><br>
|
||||
{{ _("Coinkite disclosed a firmware flaw that reduced generated-seed entropy on ColdCard Mk2/Mk3 (firmware 4.0.1-4.1.9) and Mk4/Mk5/Q before their fixed firmware. If your seed was generated on-device with fewer than 50 independent dice rolls, update firmware, generate a new seed and move your funds.") }}
|
||||
<a class="text-orange-700 underline" href="https://blog.coinkite.com/coldcard-mk3-seed-generation-warning/" target="_blank" rel="noopener noreferrer">{{ _("Read Coinkite's advisory") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="coldcard-instructions" class="{%if device_class.device_type != 'coldcard' %}hidden{% endif %}">
|
||||
<p>{{ _("Connect your ColdCard to the computer via USB and unlock it or upload a wallet export file from micro SD card.") }}</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
|
||||
<p class="mt-3 mb-0 selection-button cursor-pointer" data-style="text-decoration: underline; cursor: pointer;" onclick="showPageOverlay('usb_import_account_number')">{{ _("Specify account number (advanced)") }}</p>
|
||||
|
||||
<div class="flex space-x-3 mt-3">
|
||||
<div class="flex space-x-3 mt-3 relative z-20">
|
||||
<qr-scanner id="xpub-scan">
|
||||
<a slot="button" href="#" class="button mb-3">
|
||||
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" data-style="width: 26px; margin-right: 2px;" class="svg-white"> {{ _("Scan QR Code") }}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,14 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex p-4 mt-3 mb-4 bg-dark-600 rounded-lg" role="alert">
|
||||
<svg class="flex-shrink-0 inline w-6 h-6 mr-3" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><!--Generated by IJSVG (https://github.com/iconjar/IJSVG)--><path d="M32.0022,55.9844l-1.04907e-06,-2.84217e-14c-13.2548,-5.79387e-07 -24,-10.7452 -24,-24c5.79387e-07,-13.2548 10.7452,-24 24,-24c13.2548,5.79387e-07 24,10.7452 24,24l2.13163e-14,-1.04907e-06c0,13.2548 -10.7452,24 -24,24Zm1.5,-32l3.82016e-08,-2.71086e-09c-1.79133,0.127116 -3.35165,-1.21031 -3.5,-3l6.50646e-08,-7.8524e-07c0.148296,-1.78972 1.70866,-3.12717 3.5,-3l-5.85322e-09,4.15529e-10c1.79134,-0.127171 3.3517,1.21028 3.5,3l-5.57104e-08,6.72365e-07c-0.148295,1.78976 -1.70872,3.12723 -3.5001,3Zm1.4206,6.7635l-2.7853,10.5093l-8.37179e-08,3.15667e-07c-0.270677,1.02062 0.262417,2.08281 1.2425,2.4757l-5.91015e-08,-2.36708e-08c0.379505,0.151996 0.628277,0.519688 0.6282,0.9285v0.323l8.52651e-14,4.05e-07c0,0.551933 -0.447167,0.999503 -0.9991,1h-2.9891l-4.04477e-08,1.96948e-10c-1.65938,0.00807986 -3.01111,-1.33056 -3.01919,-2.98993c-0.00127082,-0.260991 0.0314695,-0.521035 0.0973943,-0.773566l2.7852,-10.5094l2.79671e-08,-1.05468e-07c0.270652,-1.02067 -0.262537,-2.08289 -1.2427,-2.4757l-2.20542e-08,-8.83296e-09c-0.379505,-0.151996 -0.628276,-0.519688 -0.6282,-0.9285v-0.3229l-1.77636e-14,-9.40042e-08c-8.33514e-08,-0.552011 0.447289,-0.999614 0.9993,-1h2.9887l1.06497e-07,-5.28143e-10c1.65943,-0.00822943 3.01133,1.33033 3.01956,2.98976c0.00129458,0.261047 -0.0314356,0.52115 -0.0973639,0.773738Z" fill="currentColor" fill-rule="evenodd"></path></svg>
|
||||
<span class="sr-only">Info</span>
|
||||
<div>
|
||||
{{ _("Specter authentication protects access to the app, but it does not encrypt the whole Specter data folder at rest. Wallet metadata such as addresses, labels, and xpubs may be readable from files on disk. Use OS disk encryption or an encrypted volume for the Specter data folder if other users, malware, backup tools, or forensic access are in your threat model.") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="hasencryptedservicedata" class="flex p-4 mt-3 mb-4 bg-dark-600 rounded-lg" role="alert">
|
||||
<svg class="flex-shrink-0 inline w-6 h-6 mr-3" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><!--Generated by IJSVG (https://github.com/iconjar/IJSVG)--><path d="M32.0022,55.9844l-1.04907e-06,-2.84217e-14c-13.2548,-5.79387e-07 -24,-10.7452 -24,-24c5.79387e-07,-13.2548 10.7452,-24 24,-24c13.2548,5.79387e-07 24,10.7452 24,24l2.13163e-14,-1.04907e-06c0,13.2548 -10.7452,24 -24,24Zm1.5,-32l3.82016e-08,-2.71086e-09c-1.79133,0.127116 -3.35165,-1.21031 -3.5,-3l6.50646e-08,-7.8524e-07c0.148296,-1.78972 1.70866,-3.12717 3.5,-3l-5.85322e-09,4.15529e-10c1.79134,-0.127171 3.3517,1.21028 3.5,3l-5.57104e-08,6.72365e-07c-0.148295,1.78976 -1.70872,3.12723 -3.5001,3Zm1.4206,6.7635l-2.7853,10.5093l-8.37179e-08,3.15667e-07c-0.270677,1.02062 0.262417,2.08281 1.2425,2.4757l-5.91015e-08,-2.36708e-08c0.379505,0.151996 0.628277,0.519688 0.6282,0.9285v0.323l8.52651e-14,4.05e-07c0,0.551933 -0.447167,0.999503 -0.9991,1h-2.9891l-4.04477e-08,1.96948e-10c-1.65938,0.00807986 -3.01111,-1.33056 -3.01919,-2.98993c-0.00127082,-0.260991 0.0314695,-0.521035 0.0973943,-0.773566l2.7852,-10.5094l2.79671e-08,-1.05468e-07c0.270652,-1.02067 -0.262537,-2.08289 -1.2427,-2.4757l-2.20542e-08,-8.83296e-09c-0.379505,-0.151996 -0.628276,-0.519688 -0.6282,-0.9285v-0.3229l-1.77636e-14,-9.40042e-08c-8.33514e-08,-0.552011 0.447289,-0.999614 0.9993,-1h2.9887l1.06497e-07,-5.28143e-10c1.65943,-0.00822943 3.01133,1.33033 3.01956,2.98976c0.00129458,0.261047 -0.0314356,0.52115 -0.0973639,0.773738Z" fill="currentColor" fill-rule="evenodd"></path></svg>
|
||||
<span class="sr-only">Info</span>
|
||||
|
|
|
|||
|
|
@ -463,10 +463,10 @@ class User(UserMixin):
|
|||
|
||||
def verify_jwt_token_id_and_jwt_token(self, jwt_token_id, jwt_token):
|
||||
# Verifying the JWT token ID and JWT token
|
||||
if jwt_token_id in self.jwt_tokens:
|
||||
if self.jwt_tokens[jwt_token_id]["jwt_token"] == jwt_token:
|
||||
return True
|
||||
return False
|
||||
if not isinstance(self.jwt_tokens, dict) or not isinstance(jwt_token_id, str):
|
||||
return False
|
||||
token_info = self.jwt_tokens.get(jwt_token_id)
|
||||
return isinstance(token_info, dict) and token_info.get("jwt_token") == jwt_token
|
||||
|
||||
def get_jwt_token(self, jwt_token_id):
|
||||
# Getting a JWT token from the hashmap by ID
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import os
|
|||
from pathlib import Path
|
||||
import pkgutil
|
||||
from pkgutil import iter_modules
|
||||
import re
|
||||
import sys
|
||||
from typing import List
|
||||
from .common import camelcase2snake_case
|
||||
from ..specter_error import SpecterError, SpecterInternalException
|
||||
from .shell import grep
|
||||
|
||||
from .reflection_fs import detect_extension_style_in_cwd, search_dirs_in_path
|
||||
|
||||
|
|
@ -130,6 +130,29 @@ def get_classlist_of_type_clazz_from_modulelist(clazz, modulelist, skip_missing=
|
|||
return class_list
|
||||
|
||||
|
||||
def is_specter_desktop_project(cwd=".") -> bool:
|
||||
"""Whether cwd is the specter-desktop project itself rather than an
|
||||
extension-project. Detected via the project-name in the pyproject.toml.
|
||||
Hmm, a bit hackish but we don't want to depend on toml-parsing libs.
|
||||
"""
|
||||
try:
|
||||
with open(Path(cwd, "pyproject.toml")) as pyproject_file:
|
||||
for line in pyproject_file:
|
||||
line = line.strip().replace(" ", "").replace("'", "").replace('"', "")
|
||||
if not line.startswith("name="):
|
||||
continue
|
||||
# PEP 503: ".", "-" and "_" are equivalent in project-names, so
|
||||
# "cryptoadvance.specter" and "cryptoadvance_specter" are the same
|
||||
name = re.sub(r"[-_.]+", "-", line[len("name=") :]).lower()
|
||||
if name == "cryptoadvance-specter":
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
# Expected for adhoc-style extension-projects: those have no
|
||||
# pyproject.toml at all and are therefore not specter-desktop
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def get_subclasses_for_clazz_in_cwd(clazz, cwd=".") -> List[type]:
|
||||
"""Returns all subclasses of class clazz located in the CWD if the cwd
|
||||
is not a specter-desktop dev-env-kind-of-dir or contains any .py-file
|
||||
|
|
@ -140,37 +163,26 @@ def get_subclasses_for_clazz_in_cwd(clazz, cwd=".") -> List[type]:
|
|||
return []
|
||||
|
||||
# if not testing but in a folder which looks like specter-desktop/src --> No dynamic extensions
|
||||
if "PYTEST_CURRENT_TEST" not in os.environ:
|
||||
# Hmm, a bit hackish but if the pyproject.toml specifies cryptoadvance.specter as a name and
|
||||
# we don't need to depend on toml-parsing libs, that should be ok.
|
||||
try:
|
||||
found, line = grep("./pyproject.toml", 'name = "cryptoadvance.specter"')
|
||||
if found:
|
||||
return []
|
||||
if line:
|
||||
line = line.replace(" ", "").replace("'", "").replace('"', "")
|
||||
if line == "name=cryptoadvance.specter":
|
||||
return []
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
if "PYTEST_CURRENT_TEST" not in os.environ and is_specter_desktop_project(cwd):
|
||||
return []
|
||||
|
||||
# Depending on the style we either add "." or "./src" to the searchpath
|
||||
# Depending on the style we either add cwd or cwd/src to the searchpath
|
||||
|
||||
extension_style = detect_extension_style_in_cwd()
|
||||
extension_style = detect_extension_style_in_cwd(cwd)
|
||||
# raise Exception(extension_style)
|
||||
if extension_style == "adhoc":
|
||||
package_dirs.append(Path("."))
|
||||
package_dirs.append(Path(cwd))
|
||||
elif extension_style == "publish-ready":
|
||||
package_dirs.extend(search_dirs_in_path(Path("./src")))
|
||||
package_dirs.extend(search_dirs_in_path(Path(cwd, "src")))
|
||||
elif extension_style == "specter-desktop":
|
||||
if "PYTEST_CURRENT_TEST" in os.environ:
|
||||
# I admit, ugly hack
|
||||
logger.info("We're in testing mode. Adding CWD to searchpath")
|
||||
package_dirs.append(Path("./src"))
|
||||
package_dirs.append(Path(cwd, "src"))
|
||||
else:
|
||||
raise Exception(
|
||||
f"""
|
||||
We checked before that we're not in the specter-desktop home
|
||||
We checked before that we're not in the specter-desktop home
|
||||
directory but now the extension-style is 'specter-desktop' ?!
|
||||
This should not happen!
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -354,6 +354,40 @@ class Wallet(AbstractWallet):
|
|||
res = wallet_rpc.importdescriptors(args)
|
||||
else:
|
||||
res = wallet_rpc.importmulti(args, {"rescan": False})
|
||||
if not all([r["success"] for r in res]):
|
||||
# Core refuses to shrink an existing keypool range on
|
||||
# (re-)import: "new range must include current range".
|
||||
# This can happen when re-creating a wallet under a name
|
||||
# that Core already has a wider range recorded for (e.g.
|
||||
# after a keypoolrefill). Parse the range Core is telling
|
||||
# us about and retry once with a range wide enough to
|
||||
# include it (see #2604).
|
||||
widened_range = None
|
||||
for r in res:
|
||||
if r["success"]:
|
||||
continue
|
||||
match = re.search(
|
||||
r"current range = \[(\d+),\s*(\d+)\]",
|
||||
r.get("error", {}).get("message", ""),
|
||||
)
|
||||
if not match:
|
||||
continue
|
||||
current_start, current_end = int(match.group(1)), int(
|
||||
match.group(2)
|
||||
)
|
||||
start = min(0, current_start)
|
||||
end = max(cls.GAP_LIMIT, current_end)
|
||||
if widened_range is None:
|
||||
widened_range = [start, end]
|
||||
else:
|
||||
widened_range = [
|
||||
min(widened_range[0], start),
|
||||
max(widened_range[1], end),
|
||||
]
|
||||
if widened_range is not None:
|
||||
for arg in args:
|
||||
arg["range"] = widened_range
|
||||
res = wallet_rpc.importmulti(args, {"rescan": False})
|
||||
|
||||
if not all([r["success"] for r in res]):
|
||||
all_issues = " and ".join(
|
||||
|
|
|
|||
|
|
@ -137,12 +137,25 @@ class SwanClient:
|
|||
).decode()
|
||||
auth_header["Authorization"] = f"Basic {auth_hash}"
|
||||
|
||||
response = requests.post(
|
||||
f"{self.api_url}/oidc/token",
|
||||
data=payload,
|
||||
headers=auth_header,
|
||||
)
|
||||
resp = json.loads(response.text)
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.api_url}/oidc/token",
|
||||
data=payload,
|
||||
headers=auth_header,
|
||||
timeout=30,
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.exception(e)
|
||||
raise SwanApiException(
|
||||
f"Could not reach the Swan API ({self.api_url}/oidc/token): {e}"
|
||||
) from e
|
||||
try:
|
||||
resp = json.loads(response.text)
|
||||
except ValueError as e:
|
||||
logger.error(f"{response.status_code}: {response.text}")
|
||||
raise SwanApiException(
|
||||
f"Swan API returned no valid json ({response.status_code}): {response.text}"
|
||||
) from e
|
||||
"""
|
||||
{
|
||||
"access_token": "***************",
|
||||
|
|
@ -184,27 +197,43 @@ class SwanClient:
|
|||
"User-Agent": "Specter Desktop",
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
}
|
||||
request_context = f"endpoint: {self.api_url}{endpoint} | method: {method} | payload: {json.dumps(json_payload, indent=4)}"
|
||||
|
||||
try:
|
||||
if method == "GET":
|
||||
response = requests.get(self.api_url + endpoint, headers=auth_header)
|
||||
response = requests.get(
|
||||
self.api_url + endpoint, headers=auth_header, timeout=30
|
||||
)
|
||||
elif method in ["POST", "PATCH", "PUT", "DELETE"]:
|
||||
response = requests.request(
|
||||
method=method,
|
||||
url=self.api_url + endpoint,
|
||||
headers=auth_header,
|
||||
json=json_payload,
|
||||
timeout=30,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
raise SwanApiException(f"{response.status_code}: {response.text}")
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
# TODO: tighten up expected Exceptions
|
||||
else:
|
||||
raise SwanApiException(f"Unsupported method: {method}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
# Timeouts, connection errors, ... : no response to report about
|
||||
logger.exception(e)
|
||||
logger.error(
|
||||
f"endpoint: {self.api_url}{endpoint} | method: {method} | payload: {json.dumps(json_payload, indent=4)}"
|
||||
)
|
||||
logger.error(request_context)
|
||||
raise SwanApiException(f"Could not reach the Swan API: {e}") from e
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(request_context)
|
||||
logger.error(f"{response.status_code}: {response.text}")
|
||||
raise e
|
||||
raise SwanApiException(f"{response.status_code}: {response.text}")
|
||||
|
||||
try:
|
||||
return response.json()
|
||||
except ValueError as e:
|
||||
logger.exception(e)
|
||||
logger.error(request_context)
|
||||
logger.error(f"{response.status_code}: {response.text}")
|
||||
raise SwanApiException(
|
||||
f"Swan API returned no valid json ({response.status_code}): {response.text}"
|
||||
) from e
|
||||
|
||||
def get_autowithdrawal_addresses(self, swan_wallet_id: str) -> dict:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -89,9 +89,24 @@ def pytest_addoption(parser):
|
|||
default="master",
|
||||
help="Version of elementsd (something which works with git checkout ...)",
|
||||
)
|
||||
parser.addoption(
|
||||
"--run-jade-hardware",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Run tests marked jade_hardware (real Jade attached + operator).",
|
||||
)
|
||||
listen()
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
if config.getoption("--run-jade-hardware"):
|
||||
return
|
||||
skip = pytest.mark.skip(reason="opt-in via --run-jade-hardware")
|
||||
for item in items:
|
||||
if item.get_closest_marker("jade_hardware") is not None:
|
||||
item.add_marker(skip)
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
# ToDo: use custom compiled version of bitcoind
|
||||
# E.g. test again bitcoind version [currentRelease] + master-branch
|
||||
|
|
|
|||
1
tests/fixtures/jade_hardware.psbt
vendored
Normal file
1
tests/fixtures/jade_hardware.psbt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
cHNidP8BAHECAAAAAYz9aGwGnHSzsZXBuDlKg9hNl6KeLvo8czy6WHaPTGwGAAAAAAD/////AqyEAQAAAAAAFgAUb6AWUAo8anN+uyYOLdyni6kjRVishAEAAAAAABYAFC80qhzwClOwVaKRoDp9RfCmmItSAAAAAAABAFMCAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wFR/////wFADQMAAAAAABYAFNDEo+8J6Ze26Z45flGP4+QaEYyhAAAAAAEBH0ANAwAAAAAAFgAU0MSj7wnpl7bpnjl+UY/j5BoRjKEiBgLnqyU3tdSelwMJquBunknzbOHJ/rvUTsjg0cygtPnDGRhzxdoKVAAAgAEAAIAAAACAAAAAAAAAAAAAACICA11J7M1U0AmeQ2did8em1GJdYR2oil30m/lReneRp3elGHPF2gpUAACAAQAAgAAAAIABAAAAAAAAAAA=
|
||||
BIN
tests/fixtures/jade_seedqr_abandon.png
vendored
Normal file
BIN
tests/fixtures/jade_seedqr_abandon.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 354 B |
15
tests/fixtures/jade_seedqr_abandon.txt
vendored
Normal file
15
tests/fixtures/jade_seedqr_abandon.txt
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
█████████████████████████████
|
||||
██ ▄▄▄▄▄ ██▄▀▀ ▀▄▀▄█ ▄▄▄▄▄ ██
|
||||
██ █ █ █▄ ▀▄▀▄▀▄█ █ █ ██
|
||||
██ █▄▄▄█ ██▀▄██▀▄▀▄█ █▄▄▄█ ██
|
||||
██▄▄▄▄▄▄▄█ ▀ █ ▀▄▀▄█▄▄▄▄▄▄▄██
|
||||
██ ▄▀ ▄▄▄▄█ █▀▄▀▄ ▄▀█▀▄▀███
|
||||
██ ▀██▄▀▄▀▀▄█▀██▄▀▄▀▄▀▄▀▄▀▄██
|
||||
████▀▄▄▄▄▀▀██▀ ▀█▀▄▀▄▀▄▀▄▀▄██
|
||||
██▄▀█ ██▄▀██ ▄ ▀▄▀▄▀▄▀▄▀▄▀▄██
|
||||
██▄██▄▄▄▄▄▀▀▀ ██▄▀ ▄▄▄ ▀▄▀▄██
|
||||
██ ▄▄▄▄▄ █ ▄ ▀█▄█▀ █▄█ ▀▄▀▄██
|
||||
██ █ █ █▄▄ ▀ ▀█▀▄ ▄ ▄▀▄ ███
|
||||
██ █▄▄▄█ █ ▀█▄ ▀ ▀▄▀▄▀▄▀▄█▄██
|
||||
██▄▄▄▄▄▄▄█▄█▄▄██▄█▄█▄█▄█▄█▄██
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
|
|
@ -317,14 +317,18 @@ function gpg_verify_sums {
|
|||
fi
|
||||
local imported=0
|
||||
for fpr in "${keys[@]}"; do
|
||||
# Try keys.openpgp.org first, then keyserver.ubuntu.com.
|
||||
if curl -fsSL "https://keys.openpgp.org/vks/v1/by-fingerprint/${fpr}" 2>/dev/null \
|
||||
| gpg --import 2>/dev/null; then
|
||||
imported=$((imported + 1))
|
||||
continue
|
||||
fi
|
||||
if curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${fpr}&options=mr" 2>/dev/null \
|
||||
| gpg --import 2>/dev/null; then
|
||||
# Import from both sources. Some keyservers may return partial key
|
||||
# material for old signatures (e.g. missing signing subkeys).
|
||||
# Importing from both increases robustness while keeping the same
|
||||
# pinned trust anchors for verification.
|
||||
curl -fsSL "https://keys.openpgp.org/vks/v1/by-fingerprint/${fpr}" 2>/dev/null \
|
||||
| gpg --import 2>/dev/null || true
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${fpr}&options=mr" 2>/dev/null \
|
||||
| gpg --import 2>/dev/null || true
|
||||
|
||||
# Count this key as imported if it exists in the keyring after the
|
||||
# multi-source import attempts.
|
||||
if gpg --list-keys --with-colons "$fpr" 2>/dev/null | grep -q '^pub:'; then
|
||||
imported=$((imported + 1))
|
||||
fi
|
||||
done
|
||||
|
|
|
|||
180
tests/test_hwi_server.py
Normal file
180
tests/test_hwi_server.py
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import re
|
||||
import sys
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from cryptoadvance.specter.config import TestConfig
|
||||
from cryptoadvance.specter.helpers import hwi_get_config
|
||||
from cryptoadvance.specter.server import create_app, init_app
|
||||
from cryptoadvance.specter.specter import Specter
|
||||
|
||||
|
||||
def make_scoped_app(tmp_path, hwibridge, auth_method="none"):
|
||||
config_name = f"ScopedTestConfig_{uuid4().hex}"
|
||||
config_class = type(
|
||||
config_name,
|
||||
(TestConfig,),
|
||||
{
|
||||
"__module__": __name__,
|
||||
"SPECTER_DATA_FOLDER": str(tmp_path),
|
||||
"SPECTER_URL_PREFIX": "/spc",
|
||||
"SESSION_COOKIE_PATH": "/spc",
|
||||
"SESSION_PROTECTION": None,
|
||||
"SKIP_HWI_INITIALISATION_AT_STARTUP": True,
|
||||
"SPECTER_API_ACTIVE": False,
|
||||
},
|
||||
)
|
||||
setattr(sys.modules[__name__], config_name, config_class)
|
||||
specter = Specter(data_folder=str(tmp_path), checker_threads=False)
|
||||
if auth_method != "none":
|
||||
specter.update_auth(auth_method, 10, 1)
|
||||
|
||||
app = create_app(config_class)
|
||||
app.config["TESTING"] = True
|
||||
with app.app_context():
|
||||
init_app(app, hwibridge=hwibridge, specter=specter)
|
||||
return app
|
||||
|
||||
|
||||
def csrf_token(response):
|
||||
match = re.search(b'name="csrf_token" value="([^"]+)', response.data)
|
||||
assert match is not None
|
||||
return match.group(1).decode()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hwibridge", [False, True])
|
||||
def test_hwi_settings_use_session_scoped_route(tmp_path, hwibridge):
|
||||
app = make_scoped_app(tmp_path, hwibridge)
|
||||
client = app.test_client()
|
||||
|
||||
legacy_response = client.get("/hwi/settings/")
|
||||
assert legacy_response.status_code == 302
|
||||
assert legacy_response.headers["Location"] == "/spc/hwi/settings/"
|
||||
|
||||
settings_response = client.get("/spc/hwi/settings/")
|
||||
assert settings_response.status_code == 200
|
||||
assert "Path=/spc" in settings_response.headers["Set-Cookie"]
|
||||
|
||||
missing_csrf_response = client.post(
|
||||
"/spc/hwi/settings/",
|
||||
data={"action": "update", "whitelisted_domains": "http://attacker/"},
|
||||
)
|
||||
assert missing_csrf_response.status_code in (302, 400)
|
||||
assert hwi_get_config(app.specter)["whitelisted_domains"] != "http://attacker/"
|
||||
|
||||
invalid_csrf_response = client.post(
|
||||
"/spc/hwi/settings/",
|
||||
data={
|
||||
"action": "update",
|
||||
"csrf_token": "invalid",
|
||||
"whitelisted_domains": "http://attacker/",
|
||||
},
|
||||
)
|
||||
assert invalid_csrf_response.status_code in (302, 400)
|
||||
assert hwi_get_config(app.specter)["whitelisted_domains"] != "http://attacker/"
|
||||
|
||||
update_response = client.post(
|
||||
"/spc/hwi/settings/",
|
||||
data={
|
||||
"action": "update",
|
||||
"csrf_token": csrf_token(settings_response),
|
||||
"whitelisted_domains": "http://example.com/",
|
||||
},
|
||||
)
|
||||
assert update_response.status_code == 200
|
||||
assert (
|
||||
hwi_get_config(app.specter)["whitelisted_domains"].strip()
|
||||
== "http://example.com/"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hwibridge", [False, True])
|
||||
def test_hwi_settings_require_authenticated_admin(tmp_path, hwibridge):
|
||||
app = make_scoped_app(tmp_path, hwibridge=hwibridge, auth_method="usernamepassword")
|
||||
client = app.test_client()
|
||||
|
||||
anonymous_response = client.get("/spc/hwi/settings/")
|
||||
assert anonymous_response.status_code == 302
|
||||
assert anonymous_response.headers["Location"].startswith(
|
||||
"/spc/auth/login?next=%2Fspc%2Fhwi%2Fsettings%2F"
|
||||
)
|
||||
assert client.get("/spc/auth/login").status_code == 200
|
||||
|
||||
login_response = client.post(
|
||||
"/spc/auth/login",
|
||||
data={
|
||||
"username": "admin",
|
||||
"password": "admin",
|
||||
"next": "/spc/hwi/settings/",
|
||||
},
|
||||
)
|
||||
assert login_response.status_code == 302
|
||||
assert login_response.headers["Location"] == "/spc/hwi/settings/"
|
||||
assert client.get(login_response.headers["Location"]).status_code == 200
|
||||
|
||||
app.specter.user_manager.create_user(
|
||||
user_id="nonadmin",
|
||||
username="nonadmin",
|
||||
plaintext_password="nonadmin",
|
||||
config={},
|
||||
)
|
||||
nonadmin_client = app.test_client()
|
||||
nonadmin_login_response = nonadmin_client.post(
|
||||
"/spc/auth/login",
|
||||
data={
|
||||
"username": "nonadmin",
|
||||
"password": "nonadmin",
|
||||
"next": "/spc/hwi/settings/",
|
||||
},
|
||||
)
|
||||
assert nonadmin_login_response.status_code == 302
|
||||
assert nonadmin_client.get("/spc/hwi/settings/").status_code == 403
|
||||
|
||||
|
||||
def test_hwi_settings_force_admin_when_login_disabled(tmp_path):
|
||||
app = make_scoped_app(tmp_path, hwibridge=True, auth_method="usernamepassword")
|
||||
app.specter.user_manager.create_user(
|
||||
user_id="nonadmin",
|
||||
username="nonadmin",
|
||||
plaintext_password="nonadmin",
|
||||
config={},
|
||||
)
|
||||
client = app.test_client()
|
||||
login_response = client.post(
|
||||
"/spc/auth/login",
|
||||
data={
|
||||
"username": "nonadmin",
|
||||
"password": "nonadmin",
|
||||
"next": "/spc/hwi/settings/",
|
||||
},
|
||||
)
|
||||
assert login_response.status_code == 302
|
||||
|
||||
app.specter.update_auth("none", 10, 1)
|
||||
app.config["LOGIN_DISABLED"] = True
|
||||
assert client.get("/spc/hwi/settings/").status_code == 200
|
||||
with client.session_transaction(path="/spc/hwi/settings/") as session:
|
||||
assert session["_user_id"] == "admin"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hwibridge", [False, True])
|
||||
def test_hwi_api_remains_csrf_exempt(tmp_path, hwibridge):
|
||||
app = make_scoped_app(tmp_path, hwibridge=hwibridge)
|
||||
app.specter.hwi.exposed_rpc["enumerate"] = lambda **kwargs: []
|
||||
client = app.test_client()
|
||||
client.environ_base["HTTP_ORIGIN"] = "http://127.0.0.1:25441/"
|
||||
|
||||
response = client.post(
|
||||
"/hwi/api/",
|
||||
json={
|
||||
"jsonrpc": "2.0",
|
||||
"method": "enumerate",
|
||||
"id": 1,
|
||||
"params": {},
|
||||
"forwarded_request": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.get_json() == {"id": 1, "jsonrpc": "2.0", "result": []}
|
||||
140
tests/test_jade_hardware.py
Normal file
140
tests/test_jade_hardware.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
"""Opt-in tests requiring a physical Blockstream Jade attached + operator.
|
||||
|
||||
Skipped by default. Enable with::
|
||||
|
||||
pytest --run-jade-hardware tests/test_jade_hardware.py -s
|
||||
|
||||
The ``-s`` is required so operator prompts reach your terminal.
|
||||
|
||||
Operator setup
|
||||
--------------
|
||||
For ``test_jade_enumerate_via_specter`` and
|
||||
``test_jade_extract_xpub_via_specter``: any Jade with any seed (your
|
||||
real one is fine — those tests only read public material).
|
||||
|
||||
For ``test_jade_sign_psbt_via_specter``: the Jade must be in
|
||||
**Temporary Signer mode** with the public **BIP-39 abandon vector** seed
|
||||
(``abandon abandon ... about``) loaded. The simplest path:
|
||||
|
||||
1. Power-cycle the Jade so it's at the boot menu.
|
||||
2. Choose ``Temporary Signer`` -> ``Scan SeedQR``.
|
||||
3. Scan ``tests/fixtures/jade_seedqr_abandon.png`` (or display
|
||||
``tests/fixtures/jade_seedqr_abandon.txt`` and scan from screen).
|
||||
4. Confirm the **testnet** network on Jade.
|
||||
5. The PSBT at ``tests/fixtures/jade_hardware.psbt`` is fabricated by
|
||||
Coldcard's psbt_faker against this exact seed.
|
||||
|
||||
Temporary Signer state lives in RAM only; it's wiped on power-cycle. Your
|
||||
real seed is not affected.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from cryptoadvance.specter.hwi_rpc import HWIBridge
|
||||
|
||||
|
||||
FIXTURE_DIR = Path(__file__).parent / "fixtures"
|
||||
PSBT_FIXTURE = FIXTURE_DIR / "jade_hardware.psbt"
|
||||
SEEDQR_PNG = FIXTURE_DIR / "jade_seedqr_abandon.png"
|
||||
SEEDQR_TXT = FIXTURE_DIR / "jade_seedqr_abandon.txt"
|
||||
|
||||
ABANDON_FINGERPRINT = "73c5da0a"
|
||||
|
||||
|
||||
def _enumerate_jade(bridge: HWIBridge, chain: str = "main"):
|
||||
# HWIBridge.enumerate defaults chain="" which Chain.argparse passes
|
||||
# through unchanged; Jade's enumerate then fails with
|
||||
# "Unhandled network: ". Pass an explicit chain.
|
||||
devs = bridge.enumerate(chain=chain)
|
||||
return [d for d in devs if d.get("type") == "jade"]
|
||||
|
||||
|
||||
def _prompt(msg: str) -> None:
|
||||
print(f"\n>>> {msg}")
|
||||
try:
|
||||
input(">>> Press Enter when ready... ")
|
||||
except EOFError:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jade_hardware
|
||||
def test_jade_enumerate_via_specter():
|
||||
"""Jade is detected by Specter's HWIBridge and reports a fingerprint."""
|
||||
_prompt("Connect and unlock the Jade.")
|
||||
bridge = HWIBridge(skip_hwi_initialisation=True)
|
||||
jades = _enumerate_jade(bridge)
|
||||
assert jades, "no Jade detected — connect, unlock, and rerun"
|
||||
jade = jades[0]
|
||||
assert jade.get("fingerprint"), f"Jade enumerated without fingerprint: {jade}"
|
||||
assert jade.get("path"), f"Jade enumerated without path: {jade}"
|
||||
|
||||
|
||||
@pytest.mark.jade_hardware
|
||||
def test_jade_extract_xpub_via_specter():
|
||||
"""Specter can pull an xpub at a known derivation from Jade."""
|
||||
_prompt("Unlock the Jade. You may be asked to confirm the xpub export.")
|
||||
bridge = HWIBridge(skip_hwi_initialisation=True)
|
||||
jades = _enumerate_jade(bridge)
|
||||
assert jades, "no Jade detected"
|
||||
fingerprint = jades[0].get("fingerprint")
|
||||
assert fingerprint, f"Jade enumerated without fingerprint: {jades[0]}"
|
||||
|
||||
# chain must be passed explicitly: HWIBridge.extract_xpub default is
|
||||
# chain="" which Specter's JadeClient.__init__ rejects via _network()
|
||||
# before extract_xpub's post-init override can apply.
|
||||
xpub_line = bridge.extract_xpub(
|
||||
derivation="m/84h/0h/0h",
|
||||
device_type="jade",
|
||||
fingerprint=fingerprint,
|
||||
chain="main",
|
||||
)
|
||||
assert xpub_line, "extract_xpub returned empty"
|
||||
assert xpub_line.startswith("["), f"unexpected format: {xpub_line!r}"
|
||||
assert "]" in xpub_line, f"unexpected format: {xpub_line!r}"
|
||||
body = xpub_line.split("]", 1)[1].strip()
|
||||
assert body.startswith(("xpub", "zpub", "ypub")), f"unexpected xpub: {body[:8]}"
|
||||
|
||||
|
||||
@pytest.mark.jade_hardware
|
||||
def test_jade_sign_psbt_via_specter():
|
||||
"""End-to-end: Specter signs the canned abandon-vector PSBT through Jade.
|
||||
|
||||
Requires Jade in Temporary Signer mode with the abandon-vector seed
|
||||
(see module docstring). The fixture PSBT was generated by Coldcard's
|
||||
psbt_faker against m/84'/1'/0' on testnet; xfp is 73c5da0a.
|
||||
"""
|
||||
assert PSBT_FIXTURE.exists(), f"missing fixture: {PSBT_FIXTURE}"
|
||||
|
||||
seedqr_hint = (
|
||||
f"\n PNG: {SEEDQR_PNG}\n"
|
||||
f" ASCII: cat {SEEDQR_TXT}"
|
||||
)
|
||||
_prompt(
|
||||
"Put Jade in Temporary Signer mode -> Scan SeedQR -> select TESTNET."
|
||||
f"\n SeedQR for the BIP-39 abandon-vector lives at:{seedqr_hint}\n"
|
||||
" Then confirm the transaction on device when prompted."
|
||||
)
|
||||
|
||||
psbt_b64 = PSBT_FIXTURE.read_text().strip()
|
||||
|
||||
bridge = HWIBridge(skip_hwi_initialisation=True)
|
||||
jades = _enumerate_jade(bridge, chain="test")
|
||||
assert jades, "no Jade detected"
|
||||
fingerprint = jades[0].get("fingerprint")
|
||||
assert fingerprint, f"Jade enumerated without fingerprint: {jades[0]}"
|
||||
assert fingerprint.lower() == ABANDON_FINGERPRINT, (
|
||||
f"connected Jade fingerprint is {fingerprint}; "
|
||||
f"expected {ABANDON_FINGERPRINT} (abandon-vector). "
|
||||
"Are you in Temporary Signer mode with the right SeedQR?"
|
||||
)
|
||||
|
||||
signed = bridge.sign_tx(
|
||||
psbt=psbt_b64,
|
||||
device_type="jade",
|
||||
fingerprint=fingerprint,
|
||||
chain="test",
|
||||
)
|
||||
assert signed, "sign_tx returned empty"
|
||||
assert signed != psbt_b64, "PSBT was returned unsigned"
|
||||
|
|
@ -82,6 +82,99 @@ def test_token_endpoints(client, empty_data_folder, caplog):
|
|||
assert data["jwt_token_life"] == 360
|
||||
|
||||
jwt_token_id = data["jwt_token_id"]
|
||||
jwt_token = data["jwt_token"]
|
||||
|
||||
# API-created tokens remain registered after the user store is reloaded.
|
||||
client.application.specter.user_manager.update()
|
||||
|
||||
# An active, registered token authenticates successfully. The missing wallet
|
||||
# is rejected by authorization after authentication has completed.
|
||||
token_headers = {"Authorization": "Bearer " + jwt_token}
|
||||
response = client.get(
|
||||
"/api/v1alpha/wallets/missing/psbt",
|
||||
follow_redirects=True,
|
||||
headers=token_headers,
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
# A signed token must still be registered in the user's active token store.
|
||||
unregistered_token = User.generate_jwt_token(
|
||||
"someuser", User.generate_token_id(), "unregistered", 360
|
||||
)
|
||||
response = client.get(
|
||||
"/api/v1alpha/wallets/missing/psbt",
|
||||
follow_redirects=True,
|
||||
headers={"Authorization": "Bearer " + unregistered_token},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
# Missing and non-string token identifiers fail closed.
|
||||
expiry = datetime.datetime.utcnow() + datetime.timedelta(seconds=360)
|
||||
invalid_payloads = [
|
||||
{"username": "someuser", "exp": expiry},
|
||||
{"username": "someuser", "jwt_token_id": ["invalid"], "exp": expiry},
|
||||
]
|
||||
for invalid_payload in invalid_payloads:
|
||||
invalid_token = jwt.encode(
|
||||
invalid_payload,
|
||||
client.application.config["SECRET_KEY"],
|
||||
algorithm="HS256",
|
||||
)
|
||||
response = client.get(
|
||||
"/api/v1alpha/wallets/missing/psbt",
|
||||
follow_redirects=True,
|
||||
headers={"Authorization": "Bearer " + invalid_token},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
# A different signed token cannot borrow an active token's identifier.
|
||||
mismatched_token = User.generate_jwt_token(
|
||||
"someuser", jwt_token_id, "mismatched", 360
|
||||
)
|
||||
response = client.get(
|
||||
"/api/v1alpha/wallets/missing/psbt",
|
||||
follow_redirects=True,
|
||||
headers={"Authorization": "Bearer " + mismatched_token},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
# Malformed persisted records fail closed instead of raising an error.
|
||||
user_details = client.application.specter.user_manager.get_user_by_username(
|
||||
"someuser"
|
||||
)
|
||||
stored_token_info = user_details.jwt_tokens[jwt_token_id]
|
||||
user_details.jwt_tokens[jwt_token_id] = {}
|
||||
user_details.save_info()
|
||||
client.application.specter.user_manager.update()
|
||||
response = client.get(
|
||||
"/api/v1alpha/wallets/missing/psbt",
|
||||
follow_redirects=True,
|
||||
headers=token_headers,
|
||||
)
|
||||
assert response.status_code == 401
|
||||
user_details = client.application.specter.user_manager.get_user_by_username(
|
||||
"someuser"
|
||||
)
|
||||
user_details.jwt_tokens[jwt_token_id] = stored_token_info
|
||||
user_details.save_info()
|
||||
|
||||
# A malformed persisted token container also fails closed after reload.
|
||||
stored_tokens = user_details.jwt_tokens
|
||||
user_details.jwt_tokens = []
|
||||
user_details.save_info()
|
||||
client.application.specter.user_manager.update()
|
||||
response = client.get(
|
||||
"/api/v1alpha/wallets/missing/psbt",
|
||||
follow_redirects=True,
|
||||
headers=token_headers,
|
||||
)
|
||||
assert response.status_code == 401
|
||||
user_details = client.application.specter.user_manager.get_user_by_username(
|
||||
"someuser"
|
||||
)
|
||||
user_details.jwt_tokens = stored_tokens
|
||||
user_details.save_info()
|
||||
client.application.specter.user_manager.update()
|
||||
|
||||
# testing GET request
|
||||
response = client.get("/api/v1alpha/token", follow_redirects=True, headers=headers)
|
||||
|
|
@ -133,6 +226,14 @@ def test_token_endpoints(client, empty_data_folder, caplog):
|
|||
data = json.loads(response.data)
|
||||
assert data["message"] == "Token deleted"
|
||||
|
||||
# Deletion immediately revokes the bearer token.
|
||||
response = client.get(
|
||||
"/api/v1alpha/wallets/missing/psbt",
|
||||
follow_redirects=True,
|
||||
headers=token_headers,
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
# retry accessing a deleted token
|
||||
response = client.get(
|
||||
"/api/v1alpha/token/" + jwt_token_id, follow_redirects=True, headers=headers
|
||||
|
|
|
|||
|
|
@ -17,6 +17,17 @@ from cryptoadvance.specter.user import User
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_registered_jwt_token(specter, username):
|
||||
user = specter.user_manager.get_user_by_username(username)
|
||||
jwt_token_id = User.generate_token_id()
|
||||
jwt_token_life = 3600
|
||||
jwt_token = User.generate_jwt_token(
|
||||
username, jwt_token_id, "test token", jwt_token_life
|
||||
)
|
||||
user.add_jwt_token(jwt_token_id, jwt_token, "test token", jwt_token_life)
|
||||
return jwt_token
|
||||
|
||||
|
||||
def almost_equal(a: Number, b: Number, precision: float = 0.01) -> bool:
|
||||
"""
|
||||
Checks if a and b are not very different.
|
||||
|
|
@ -58,9 +69,7 @@ def test_rr_psbt_get(client, specter_regtest_configured, bitcoin_regtest, caplog
|
|||
# Admin but not authorized (admin is NOT allowed to read everything)
|
||||
headers = {
|
||||
"Authorization": "Bearer "
|
||||
+ User.generate_jwt_token(
|
||||
"admin", "tokenid", "tokendescription", random.randrange(100, 200)
|
||||
)
|
||||
+ create_registered_jwt_token(specter_regtest_configured, "admin")
|
||||
}
|
||||
result = client.get(
|
||||
"/api/v1alpha/wallets/a_simple_wallet/psbt",
|
||||
|
|
@ -76,9 +85,7 @@ def test_rr_psbt_get(client, specter_regtest_configured, bitcoin_regtest, caplog
|
|||
# Proper authorized (the wallet is owned by someuser)
|
||||
headers = {
|
||||
"Authorization": "Bearer "
|
||||
+ User.generate_jwt_token(
|
||||
"someuser", "tokenid", "tokendescription", random.randrange(100, 200)
|
||||
)
|
||||
+ create_registered_jwt_token(specter_regtest_configured, "someuser")
|
||||
}
|
||||
result = client.get(
|
||||
"/api/v1alpha/wallets/a_simple_wallet/psbt",
|
||||
|
|
@ -97,9 +104,7 @@ def test_rr_psbt_post(specter_regtest_configured, bitcoin_regtest, client, caplo
|
|||
|
||||
headers = {
|
||||
"Authorization": "Bearer "
|
||||
+ User.generate_jwt_token(
|
||||
"someuser", "tokenid", "tokendescription", random.randrange(100, 200)
|
||||
),
|
||||
+ create_registered_jwt_token(specter_regtest_configured, "someuser"),
|
||||
"Content-type": "application/json",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ from unittest.mock import MagicMock
|
|||
import pytest
|
||||
import mock
|
||||
from mock import Mock, patch
|
||||
import requests
|
||||
|
||||
from cryptoadvance.specterext.swan.client import (
|
||||
SwanApiException,
|
||||
SwanApiRefreshTokenException,
|
||||
SwanClient,
|
||||
)
|
||||
|
|
@ -106,6 +109,65 @@ def test_expired_access_token():
|
|||
sc._get_access_token()
|
||||
|
||||
|
||||
def construct_client_with_valid_token():
|
||||
"""A client which won't need to fetch an access_token first"""
|
||||
return SwanClient(
|
||||
"a_hostname", "forever_valid_access_token", 5000000000, "a_refresh_token"
|
||||
)
|
||||
|
||||
|
||||
def test_authenticated_request_get_timeout(app_no_node):
|
||||
"""A timeout must surface as SwanApiException, not as an UnboundLocalError"""
|
||||
sc = construct_client_with_valid_token()
|
||||
with app_no_node.app_context():
|
||||
with mock.patch(
|
||||
"requests.get", side_effect=requests.exceptions.Timeout("simulated timeout")
|
||||
):
|
||||
with pytest.raises(SwanApiException) as exc_info:
|
||||
sc.authenticated_request("/some/endpoint")
|
||||
assert "simulated timeout" in str(exc_info.value)
|
||||
assert isinstance(exc_info.value.__cause__, requests.exceptions.Timeout)
|
||||
|
||||
|
||||
def test_authenticated_request_post_timeout(app_no_node):
|
||||
"""Same for the methods going through requests.request"""
|
||||
sc = construct_client_with_valid_token()
|
||||
with app_no_node.app_context():
|
||||
with mock.patch(
|
||||
"requests.request",
|
||||
side_effect=requests.exceptions.ConnectTimeout("simulated timeout"),
|
||||
):
|
||||
with pytest.raises(SwanApiException) as exc_info:
|
||||
sc.authenticated_request(
|
||||
"/some/endpoint", method="POST", json_payload={"muuh": "meeh"}
|
||||
)
|
||||
assert isinstance(exc_info.value.__cause__, requests.exceptions.ConnectTimeout)
|
||||
|
||||
|
||||
def test_authenticated_request_error_status_code(app_no_node):
|
||||
sc = construct_client_with_valid_token()
|
||||
fake_response = Mock()
|
||||
fake_response.status_code = 500
|
||||
fake_response.text = "Internal Server Error"
|
||||
with app_no_node.app_context():
|
||||
with mock.patch("requests.get", return_value=fake_response):
|
||||
with pytest.raises(SwanApiException, match="500: Internal Server Error"):
|
||||
sc.authenticated_request("/some/endpoint")
|
||||
|
||||
|
||||
def test_get_access_token_timeout(app_no_node):
|
||||
"""The token-endpoint is used before authenticated_request can even start"""
|
||||
sc = SwanClient("a_hostname", "an_expired_access_token", 1000, "a_refresh_token")
|
||||
with app_no_node.app_context():
|
||||
with mock.patch(
|
||||
"requests.post",
|
||||
side_effect=requests.exceptions.Timeout("simulated timeout"),
|
||||
):
|
||||
with pytest.raises(SwanApiException) as exc_info:
|
||||
sc.authenticated_request("/some/endpoint")
|
||||
assert isinstance(exc_info.value.__cause__, requests.exceptions.Timeout)
|
||||
|
||||
|
||||
@patch("requests.delete")
|
||||
@patch("requests.request")
|
||||
@patch("requests.patch")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from cryptoadvance.specter.util.reflection import (
|
|||
get_subclasses_for_clazz,
|
||||
get_subclasses_for_clazz_in_cwd,
|
||||
get_classlist_of_type_clazz_from_modulelist,
|
||||
is_specter_desktop_project,
|
||||
_get_module_from_class,
|
||||
get_package_dir_for_subclasses_of,
|
||||
search_dirs_in_path,
|
||||
|
|
@ -119,13 +120,53 @@ def test_get_classlist_raises_on_missing_module_by_default():
|
|||
get_classlist_of_type_clazz_from_modulelist(Service, modulelist)
|
||||
|
||||
|
||||
repo_root = Path(__file__).parent.parent
|
||||
xtestdata = repo_root / "tests" / "xtestdata_testextensions"
|
||||
|
||||
|
||||
def test_is_specter_desktop_project():
|
||||
"""The specter-desktop project detects itself via the name in its own
|
||||
pyproject.toml. If that name changes (PEP 503 allows "." "-" and "_" to be
|
||||
used interchangeably), the dev-server dies on startup, see #2526."""
|
||||
assert is_specter_desktop_project(repo_root)
|
||||
assert not is_specter_desktop_project(xtestdata / "ext_root_fully_qualified_1")
|
||||
assert not is_specter_desktop_project(xtestdata)
|
||||
|
||||
|
||||
def test_is_specter_desktop_project_pep503_names(tmp_path):
|
||||
for name in [
|
||||
"cryptoadvance.specter",
|
||||
"cryptoadvance_specter",
|
||||
"Cryptoadvance-Specter",
|
||||
]:
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
f'[project]\nname = "{name}"\nversion = "1.2.3"\n'
|
||||
)
|
||||
assert is_specter_desktop_project(tmp_path), f"{name} should be detected"
|
||||
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
'[project]\nname = "boatacccorp.tretboot"\n'
|
||||
)
|
||||
assert not is_specter_desktop_project(tmp_path)
|
||||
|
||||
|
||||
def test_get_subclasses_for_clazz_in_cwd_in_specter_desktop_project(monkeypatch):
|
||||
"""No dynamic extension-discovery in the specter-desktop project itself.
|
||||
Regression test: this used to raise "This should not happen!" when the
|
||||
project got renamed to cryptoadvance_specter, breaking
|
||||
`python3 -m cryptoadvance.specter server --config DevelopmentConfig`"""
|
||||
# the production code takes a shortcut for tests, so pretend we're not testing
|
||||
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
|
||||
assert get_subclasses_for_clazz_in_cwd(Service, cwd=repo_root) == []
|
||||
|
||||
|
||||
def test_get_subclasses_for_clazz_in_cwd(caplog):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
classlist: List[type] = get_subclasses_for_clazz_in_cwd(
|
||||
Service, cwd="./tests/xtestdata_testextensions"
|
||||
)
|
||||
# damn, this is difficult to test
|
||||
# assert len(classlist) == 3
|
||||
classlist: List[type] = get_subclasses_for_clazz_in_cwd(Service, cwd=xtestdata)
|
||||
# That folder is a container of extension-projects, not an extension-project
|
||||
# itself, so there is nothing importable in there
|
||||
assert classlist == []
|
||||
assert "Detected Extension-style: adhoc" in caplog.text
|
||||
|
||||
|
||||
def test_get_subclasses_for_class(caplog):
|
||||
|
|
|
|||
|
|
@ -23,7 +23,18 @@ def test_last_lines(caplog):
|
|||
assert lines[-2].startswith("OUT OF OR IN CONNECTION WITH THE SOFTWARE ")
|
||||
|
||||
|
||||
def test_grep():
|
||||
def test_grep(tmp_path):
|
||||
"""grep returns a (found, line)-tuple. Asserting on the tuple itself is
|
||||
always truthy, so always assert on the first element!"""
|
||||
from cryptoadvance.specter.util.shell import grep
|
||||
|
||||
assert grep("./pyproject.toml", 'name = "cryptoadvance.specter"')
|
||||
some_file = tmp_path / "some_file.txt"
|
||||
some_file.write_text('name = "cryptoadvance_specter"\nversion = "1.2.3"\n')
|
||||
|
||||
found, line = grep(str(some_file), 'name = "cryptoadvance_specter"')
|
||||
assert found
|
||||
assert line.strip() == 'name = "cryptoadvance_specter"'
|
||||
|
||||
found, line = grep(str(some_file), "does not exist")
|
||||
assert not found
|
||||
assert line is None
|
||||
|
|
|
|||
177
tests/test_wallet_create_range.py
Normal file
177
tests/test_wallet_create_range.py
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
"""
|
||||
Regression tests for https://github.com/cryptoadvance/specter-desktop/issues/2604
|
||||
|
||||
Bitcoin Core's legacy `importmulti` refuses to shrink the keypool range of an
|
||||
already-imported descriptor ("new range must include current range"). This can
|
||||
surface when re-creating a wallet under a name that Core already has a wider
|
||||
range recorded for. Wallet.create should detect that specific rejection and
|
||||
retry once with a range wide enough to include Core's current range, instead
|
||||
of failing outright.
|
||||
|
||||
These tests mock the RPC layer entirely, so they don't need a running
|
||||
bitcoind/regtest node.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from cryptoadvance.specter.key import Key
|
||||
from cryptoadvance.specter.wallet import Wallet
|
||||
|
||||
|
||||
def _make_rpc(importmulti_responses):
|
||||
"""Builds a fake `rpc`/`wallet_rpc` pair.
|
||||
|
||||
importmulti_responses: a list of return values, one per call to
|
||||
`importmulti`. Each element is itself the list-of-dicts the real RPC
|
||||
would return.
|
||||
"""
|
||||
wallet_rpc = MagicMock()
|
||||
wallet_rpc.importmulti.side_effect = importmulti_responses
|
||||
|
||||
rpc = MagicMock()
|
||||
rpc.getnetworkinfo.return_value = {"version": 200000} # pre-descriptor-wallets
|
||||
rpc.wallet.return_value = wallet_rpc
|
||||
return rpc, wallet_rpc
|
||||
|
||||
|
||||
class _BareWallet(Wallet):
|
||||
"""A stand-in for Wallet that skips the real __init__ (which touches
|
||||
rpc, address lists, tx history, etc.) so these tests only exercise the
|
||||
`create()` classmethod's import/retry logic."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.init_args = args
|
||||
self.init_kwargs = kwargs
|
||||
|
||||
|
||||
def _single_sig_key():
|
||||
# A known-good testnet tpub (m/84'/1'/0') used elsewhere in the test
|
||||
# suite's ghost-machine fixtures.
|
||||
tpub = "tpubDC4DsqH5rqHqipMNqUbDFtQT3AkKkUrvLsN6miySvortU3s1LGaNVAb7wX2No2VsuxQV82T8s3HJLv3kdx1CPjsJ3onC1Zo5mWCQzRVaWVX"
|
||||
return Key.parse_xpub(f"[81f802e3/84h/1h/0h]{tpub}")
|
||||
|
||||
|
||||
def test_create_retries_with_widened_range_on_shrink_refusal():
|
||||
"""Core rejects the default [0, GAP_LIMIT] range because it already has
|
||||
a wider range on file; Wallet.create should retry with a widened range
|
||||
and succeed."""
|
||||
key = _single_sig_key()
|
||||
|
||||
shrink_refusal = [
|
||||
{
|
||||
"success": False,
|
||||
"error": {"message": "new range must include current range = [0,1008]"},
|
||||
},
|
||||
{
|
||||
"success": False,
|
||||
"error": {"message": "new range must include current range = [0,1000]"},
|
||||
},
|
||||
]
|
||||
success = [{"success": True}, {"success": True}]
|
||||
|
||||
rpc, wallet_rpc = _make_rpc([shrink_refusal, success])
|
||||
|
||||
wallet = _BareWallet.create(
|
||||
rpc=rpc,
|
||||
rpc_path="specter",
|
||||
working_folder="/tmp",
|
||||
device_manager=MagicMock(),
|
||||
wallet_manager=MagicMock(),
|
||||
name="mywallet",
|
||||
alias="mywallet",
|
||||
sigs_required=1,
|
||||
key_type="wpkh",
|
||||
keys=[key],
|
||||
devices=[MagicMock()],
|
||||
core_version=200000,
|
||||
)
|
||||
|
||||
assert wallet is not None
|
||||
# One retry: the first call (default range) failed, the second (widened
|
||||
# range) succeeded.
|
||||
assert wallet_rpc.importmulti.call_count == 2
|
||||
second_call_args = wallet_rpc.importmulti.call_args_list[1][0][0]
|
||||
# Widened range must cover the widest range Core reported (1008)
|
||||
assert all(arg["range"] == [0, 1008] for arg in second_call_args)
|
||||
|
||||
|
||||
def test_create_raises_if_retry_also_fails():
|
||||
"""If the widened-range retry still fails, Wallet.create should still
|
||||
raise a SpecterError (no silent swallow of a real problem)."""
|
||||
from cryptoadvance.specter.specter_error import SpecterError
|
||||
|
||||
key = _single_sig_key()
|
||||
|
||||
shrink_refusal = [
|
||||
{
|
||||
"success": False,
|
||||
"error": {"message": "new range must include current range = [0,1008]"},
|
||||
},
|
||||
]
|
||||
still_failing = [
|
||||
{
|
||||
"success": False,
|
||||
"error": {"message": "some other unrelated error"},
|
||||
},
|
||||
]
|
||||
|
||||
rpc, wallet_rpc = _make_rpc([shrink_refusal, still_failing])
|
||||
|
||||
try:
|
||||
_BareWallet.create(
|
||||
rpc=rpc,
|
||||
rpc_path="specter",
|
||||
working_folder="/tmp",
|
||||
device_manager=MagicMock(),
|
||||
wallet_manager=MagicMock(),
|
||||
name="mywallet",
|
||||
alias="mywallet",
|
||||
sigs_required=1,
|
||||
key_type="wpkh",
|
||||
keys=[key],
|
||||
devices=[MagicMock()],
|
||||
core_version=200000,
|
||||
)
|
||||
assert False, "expected SpecterError"
|
||||
except SpecterError:
|
||||
pass
|
||||
|
||||
assert wallet_rpc.importmulti.call_count == 2
|
||||
|
||||
|
||||
def test_create_does_not_retry_on_unrelated_failure():
|
||||
"""A failure that isn't the range-shrink message should raise immediately,
|
||||
without a pointless retry."""
|
||||
from cryptoadvance.specter.specter_error import SpecterError
|
||||
|
||||
key = _single_sig_key()
|
||||
|
||||
unrelated_failure = [
|
||||
{
|
||||
"success": False,
|
||||
"error": {"message": "some other unrelated error"},
|
||||
},
|
||||
]
|
||||
|
||||
rpc, wallet_rpc = _make_rpc([unrelated_failure])
|
||||
|
||||
try:
|
||||
_BareWallet.create(
|
||||
rpc=rpc,
|
||||
rpc_path="specter",
|
||||
working_folder="/tmp",
|
||||
device_manager=MagicMock(),
|
||||
wallet_manager=MagicMock(),
|
||||
name="mywallet",
|
||||
alias="mywallet",
|
||||
sigs_required=1,
|
||||
key_type="wpkh",
|
||||
keys=[key],
|
||||
devices=[MagicMock()],
|
||||
core_version=200000,
|
||||
)
|
||||
assert False, "expected SpecterError"
|
||||
except SpecterError:
|
||||
pass
|
||||
|
||||
assert wallet_rpc.importmulti.call_count == 1
|
||||
|
|
@ -1,12 +1,8 @@
|
|||
# HW.1 / Nano
|
||||
# HW.1, Nano
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1b7c|2b7c|3b7c|4b7c", TAG+="uaccess", TAG+="udev-acl"
|
||||
# Blue
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0000|0000|0001|0002|0003|0004|0005|0006|0007|0008|0009|000a|000b|000c|000d|000e|000f|0010|0011|0012|0013|0014|0015|0016|0017|0018|0019|001a|001b|001c|001d|001e|001f", TAG+="uaccess", TAG+="udev-acl"
|
||||
# Nano S
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0001|1000|1001|1002|1003|1004|1005|1006|1007|1008|1009|100a|100b|100c|100d|100e|100f|1010|1011|1012|1013|1014|1015|1016|1017|1018|1019|101a|101b|101c|101d|101e|101f", TAG+="uaccess", TAG+="udev-acl"
|
||||
# Aramis
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0002|2000|2001|2002|2003|2004|2005|2006|2007|2008|2009|200a|200b|200c|200d|200e|200f|2010|2011|2012|2013|2014|2015|2016|2017|2018|2019|201a|201b|201c|201d|201e|201f", TAG+="uaccess", TAG+="udev-acl"
|
||||
# HW2
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0003|3000|3001|3002|3003|3004|3005|3006|3007|3008|3009|300a|300b|300c|300d|300e|300f|3010|3011|3012|3013|3014|3015|3016|3017|3018|3019|301a|301b|301c|301d|301e|301f", TAG+="uaccess", TAG+="udev-acl"
|
||||
# Nano X
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0004|4000|4001|4002|4003|4004|4005|4006|4007|4008|4009|400a|400b|400c|400d|400e|400f|4010|4011|4012|4013|4014|4015|4016|4017|4018|4019|401a|401b|401c|401d|401e|401f", TAG+="uaccess", TAG+="udev-acl"
|
||||
|
||||
# Blue, NanoS, Aramis, HW.2, Nano X, NanoSP, Stax, Ledger Test,
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", TAG+="uaccess", TAG+="udev-acl"
|
||||
|
||||
# Same, but with hidraw-based library (instead of libusb)
|
||||
KERNEL=="hidraw*", ATTRS{idVendor}=="2c97", MODE="0666"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
|
|||
@click.option("--delimiter", default=",", help="the delimiter")
|
||||
@click.argument("spec_file")
|
||||
def execute(debug, run, delimiter, spec_file):
|
||||
with open("cypress.json") as json_file:
|
||||
with open("cypress-tests.json") as json_file:
|
||||
data = json.load(json_file)
|
||||
|
||||
spec_create_list = []
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ function restore_snapshot {
|
|||
# Checking whether spec-files exists
|
||||
if ! [ -f ./cypress/integration/${spec_file} ]; then
|
||||
echo "Spec-file $spec_file does not exist, these are the options:"
|
||||
cat cypress.json | jq -r ".testFiles[]"
|
||||
cat cypress-tests.json | jq -r ".testFiles[]"
|
||||
exit 1
|
||||
fi
|
||||
snapshot_file=./cypress/fixtures/${spec_file}_btcdir.tar.gz
|
||||
|
|
@ -341,7 +341,11 @@ function sub_run {
|
|||
start_bitcoind --reset
|
||||
start_elementsd --reset
|
||||
start_specter --reset
|
||||
npx cypress run
|
||||
# Cypress 10+ normalizes specs discovered from config (often alphabetically),
|
||||
# but this suite is intentionally stateful and depends on the order in
|
||||
# cypress-tests.json. Pass an explicit --spec list so a full run keeps the
|
||||
# legacy order from cypress.json/testFiles.
|
||||
npx cypress run --spec "$(./utils/calc_cypress_test_spec.py --run spec_empty_specter_home.js)"
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
@ -358,7 +362,7 @@ function sub_snapshot {
|
|||
# We'll create a snapshot BEFORE this spec-file has been tested:
|
||||
if [ ! -f ./cypress/integration/$spec_file ]; then
|
||||
echo "ERROR: Use one of these arguments:"
|
||||
cat cypress.json | jq -r ".testFiles[]"
|
||||
cat cypress-tests.json | jq -r ".testFiles[]"
|
||||
exit 2
|
||||
fi
|
||||
start_bitcoind --reset
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue