Compare commits

...

28 commits

Author SHA1 Message Date
al-munazzim
3bcbae1a32
Pin Werkzeug below 3.1 for Flask 2.2 compatibility (#2699)
Some checks are pending
Build Docker container on push / Build image (push) Waiting to run
Extension Compatibility Check / extension-compat (push) Waiting to run
Tests / test (push) Waiting to run
Tests / cypress (push) Waiting to run
Tests / extension-smoketest (push) Waiting to run
TOC Generator / TOC Generator (push) Waiting to run
Black Python Linter / black (push) Waiting to run
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nazim <nazim@openclaw.ai>
2026-08-12 21:53:45 +02:00
al-munazzim
180801223d
Fix REST auth errors for Flask-HTTPAuth 4.8.1 (#2697)
Some checks are pending
Build Docker container on push / Build image (push) Waiting to run
Extension Compatibility Check / extension-compat (push) Waiting to run
Tests / test (push) Waiting to run
Tests / cypress (push) Waiting to run
Tests / extension-smoketest (push) Waiting to run
TOC Generator / TOC Generator (push) Waiting to run
Black Python Linter / black (push) Waiting to run
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nazim <nazim@openclaw.ai>
2026-08-12 12:17:08 +02:00
al-munazzim
07eb1520dc
Chore(deps): bundle low-risk Dependabot updates (#2696)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nazim <nazim@openclaw.ai>
2026-08-12 09:41:11 +02:00
al-munazzim
f814ac47c5
Chore(deps): bundle 5 low-risk Dependabot updates (#2693)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nazim <nazim@openclaw.ai>
Co-authored-by: k9ert <117085+k9ert@users.noreply.github.com>
2026-08-11 21:48:44 +02:00
al-munazzim
69a43ef1ea
Fix PyInstaller specterd backports import (#2694)
Co-authored-by: Nazim <nazim@openclaw.ai>
2026-08-11 21:19:55 +02:00
al-munazzim
6755046e4b
chore: bump Cypress to 13.17.0 (#2646)
Co-authored-by: Nazim <nazim@openclaw.ai>
2026-08-11 19:14:45 +02:00
ack-chai
8843e2f064
fix: retry wallet import with widened range on shrink-refusal (#2604) (#2666)
Some checks failed
Build Docker container on push / Build image (push) Has been cancelled
Electron smoketest / smoketest (push) Has been cancelled
Extension Compatibility Check / extension-compat (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / cypress (push) Has been cancelled
Tests / extension-smoketest (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
Black Python Linter / black (push) Has been cancelled
Co-authored-by: k9ert <117085+k9ert@users.noreply.github.com>
2026-08-08 22:34:48 +02:00
al-munazzim
bf0780d6bc
Protect HWI bridge settings with auth and CSRF (#2679)
Co-authored-by: Nazim <nazim@openclaw.ai>
Co-authored-by: k9ert <117085+k9ert@users.noreply.github.com>
2026-08-08 22:09:44 +02:00
dependabot[bot]
7b4d90f917
Chore(deps): Bump braces and gulp (#2668)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: k9ert <117085+k9ert@users.noreply.github.com>
2026-08-08 21:57:14 +02:00
k9ert
cf491ee5b9
Chore(deps): bundle 5 green low-risk npm dependabot bumps (#2690)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-08 21:42:22 +02:00
k9ert
9f890a90f8
Chore(deps): bundle 5 green low-risk dependabot bumps (#2689)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-08 21:12:49 +02:00
dependabot[bot]
1a3ecf07fb
Chore(deps): Bump certifi from 2024.6.2 to 2024.7.4 (#2669)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: k9ert <117085+k9ert@users.noreply.github.com>
2026-08-08 20:30:18 +02:00
al-munazzim
426ed39aac
Warn that Specter auth does not encrypt data folder (#2683)
Co-authored-by: Nazim <nazim@openclaw.ai>
2026-08-08 19:48:20 +02:00
Bunlong Heng
693338d3af
fix(swan): add timeout to remote HTTP requests to prevent hang (#2687)
Co-authored-by: k9ert <117085+k9ert@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 19:13:31 +02:00
Róbert Nagy
3d554fbc2e
Warn on ColdCard Mk3 seed-entropy advisory at device setup (#2676)
Co-authored-by: k9ert <117085+k9ert@users.noreply.github.com>
2026-08-08 18:33:01 +02:00
k9ert
48fea77b8a
fix: validate active API tokens (#2685) 2026-08-08 14:57:14 +02:00
k9ert
0b027f49fd
fix: dev-server crash in the specter-desktop repo after the project-rename (#2686)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 14:12:46 +02:00
dependabot[bot]
fe232048d4
Chore(deps-dev): Bump electron from 39.8.5 to 39.8.10 in /pyinstaller/electron (#2680)
Some checks failed
Build Docker container on push / Build image (push) Has been cancelled
Electron smoketest / smoketest (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / cypress (push) Has been cancelled
Tests / extension-smoketest (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
Black Python Linter / black (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 10:57:20 +02:00
al-munazzim
54b3616b40
fix: restore Ledger udev hidraw fallback (#2633)
Some checks failed
Build Docker container on push / Build image (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / cypress (push) Has been cancelled
Tests / extension-smoketest (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
Black Python Linter / black (push) Has been cancelled
Co-authored-by: Nazim <nazim@openclaw.ai>
Co-authored-by: k9ert <117085+k9ert@users.noreply.github.com>
2026-08-04 16:56:52 +02:00
dependabot[bot]
44c5fdeaad
Chore(deps): Bump jinja2 from 3.1.4 to 3.1.6 (#2670)
Some checks failed
Build Docker container on push / Build image (push) Has been cancelled
Extension Compatibility Check / extension-compat (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / cypress (push) Has been cancelled
Tests / extension-smoketest (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
Black Python Linter / black (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-28 21:13:02 +02:00
k9ert
febcf619d9
Bump Spectrum to 0.8.0 (#2641)
Some checks failed
Build Docker container on push / Build image (push) Has been cancelled
Extension Compatibility Check / extension-compat (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / cypress (push) Has been cancelled
Tests / extension-smoketest (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
Black Python Linter / black (push) Has been cancelled
2026-06-29 14:35:14 +02:00
k9ert
314a8579a0
deps: bump hwi 2.4.0 -> 3.1.0 (drops Python 3.7/3.8) (#2616) 2026-05-22 09:08:28 +02:00
Oren
a1a82082dd
Update specterext-timelockrecovery to v0.2.3 (#2620) 2026-05-17 21:15:57 +02:00
al-munazzim
64139ebdb7
ci: harden elements GPG key import in install_noded.sh (#2621)
Co-authored-by: Nazim <nazim@openclaw.ai>
2026-05-11 21:22:50 +02:00
k9ert
9e43afbdd0
Chore(deps): Security — high-severity pip bumps (incremental) (#2614) 2026-04-23 08:46:33 +02:00
k9ert
5a4f24295f
ci: cache electron binaries + retry downloads on 502 (#2612)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:29:09 +02:00
k9ert
a793e2dce8
test: bump tested bitcoind to v27.2.0 (#2609)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:27:32 +02:00
k9ert
6b7013066d
Chore(deps): Security — bump h11 and form-data (#2613) 2026-04-19 22:01:56 +02:00
55 changed files with 3782 additions and 8558 deletions

View file

@ -267,6 +267,15 @@ jobs:
name: specterd-linux
path: ./release-artifacts
- name: Cache Electron binaries
uses: actions/cache@v4
with:
path: |
~/.cache/electron
~/.cache/electron-builder
key: electron-linux-${{ hashFiles('pyinstaller/electron/package-lock.json') }}
restore-keys: electron-linux-
- name: Prepare Electron build
run: |
VERSION=${GITHUB_REF#refs/tags/}
@ -286,7 +295,17 @@ jobs:
cp -R ../../src/cryptoadvance/specter/static/fonts \
../../src/cryptoadvance/specter/static/output.css \
../../src/cryptoadvance/specter/static/typography.css .
npm run dist -- --linux
# Retry: electron-builder downloads from github.com occasionally 502
# (see release run 24636274855).
attempt=0
until npm run dist -- --linux; do
attempt=$((attempt + 1))
if [ $attempt -ge 3 ]; then
echo "electron-builder failed after $attempt attempts"; exit 1
fi
echo "Attempt $attempt failed; retrying in $((attempt * 15))s..."
sleep $((attempt * 15))
done
cd ../..
- name: Package release
@ -311,6 +330,10 @@ jobs:
runs-on: ubuntu-latest
container:
image: electronuserland/builder:wine
env:
# Shared HOME so `npm ci` (electron postinstall) and electron-builder
# write to the same ~/.cache/electron path — kept cacheable across runs.
HOME: /tmp/electron-home
steps:
- uses: actions/checkout@v4
with:
@ -319,6 +342,7 @@ jobs:
- name: Install system dependencies
run: |
apt-get update && apt-get install -y unzip
mkdir -p $HOME
- name: Download specterd Windows artifact
uses: actions/download-artifact@v4
@ -326,6 +350,15 @@ jobs:
name: specterd-windows
path: ./release-artifacts
- name: Cache Electron binaries
uses: actions/cache@v4
with:
path: |
/tmp/electron-home/.cache/electron
/tmp/electron-home/.cache/electron-builder
key: electron-win-${{ hashFiles('pyinstaller/electron/package-lock.json') }}
restore-keys: electron-win-
- name: Prepare Electron build
run: |
VERSION=${GITHUB_REF#refs/tags/}
@ -339,15 +372,22 @@ jobs:
cd ../..
- name: Build Electron app
env:
HOME: /tmp/electron-home
run: |
mkdir -p $HOME
cd pyinstaller/electron
cp -R ../../src/cryptoadvance/specter/static/fonts \
../../src/cryptoadvance/specter/static/output.css \
../../src/cryptoadvance/specter/static/typography.css .
npm run dist -- --win
# Retry: electron-builder downloads from github.com occasionally 502
# (see release run 24636274855).
attempt=0
until npm run dist -- --win; do
attempt=$((attempt + 1))
if [ $attempt -ge 3 ]; then
echo "electron-builder failed after $attempt attempts"; exit 1
fi
echo "Attempt $attempt failed; retrying in $((attempt * 15))s..."
sleep $((attempt * 15))
done
cd ../..
- name: Package release
@ -401,6 +441,15 @@ jobs:
name: specterd-macos-arm64
path: ./release-artifacts
- name: Cache Electron binaries
uses: actions/cache@v4
with:
path: |
~/Library/Caches/electron
~/Library/Caches/electron-builder
key: electron-mac-${{ hashFiles('pyinstaller/electron/package-lock.json') }}
restore-keys: electron-mac-
- name: Import code signing certificate
if: env.HAVE_APPLE_CERT == 'true'
env:
@ -466,7 +515,17 @@ jobs:
cp -R ../../src/cryptoadvance/specter/static/fonts \
../../src/cryptoadvance/specter/static/output.css \
../../src/cryptoadvance/specter/static/typography.css .
npm run dist -- --mac
# Retry: electron-builder downloads from github.com occasionally 502
# (see release run 24636274855).
attempt=0
until npm run dist -- --mac; do
attempt=$((attempt + 1))
if [ $attempt -ge 3 ]; then
echo "electron-builder failed after $attempt attempts"; exit 1
fi
echo "Attempt $attempt failed; retrying in $((attempt * 15))s..."
sleep $((attempt * 15))
done
cd ../..
env:
# electron-builder reads these for notarization

View file

@ -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
View file

@ -59,3 +59,7 @@ _bmad-output/
# Claude Code local workspace
.claude/
# Local agent workspaces
/.agents/
/.history/

40
cypress.config.js Normal file
View 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,
},
})

View file

@ -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')

View file

@ -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()

View file

@ -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', () => {

View file

@ -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")

View file

@ -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('/')
})

View file

@ -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

View file

@ -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)?*

7709
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,11 +13,19 @@
"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": {
"@cypress/request": {
"form-data": "^2.5.4"
}
},
"resolutions": {
"**/@cypress/request/form-data": "^2.5.4"
},
"scripts": {
"dev": "gulp",
"tailwind": "npx tailwindcss -i ./src/cryptoadvance/specter/static/style.css -o ./src/cryptoadvance/specter/static/output.css --watch",

View file

@ -19,7 +19,7 @@
"winston": "^3.13.0"
},
"devDependencies": {
"electron": "^39.8.5",
"electron": "^39.8.10",
"electron-builder": "^26.8.1"
}
},
@ -1427,7 +1427,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@ -1970,7 +1969,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@ -2012,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": {
@ -2301,7 +2299,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -2311,7 +2308,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -2321,7 +2317,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@ -2334,7 +2329,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
@ -2561,18 +2555,42 @@
}
},
"node_modules/form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
"integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.35",
"safe-buffer": "^5.2.1"
},
"engines": {
"node": ">= 0.12"
}
},
"node_modules/form-data/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
@ -2609,7 +2627,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -2628,7 +2645,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@ -2653,7 +2669,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@ -2772,7 +2787,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -2858,7 +2872,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -2871,7 +2884,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"dependencies": {
"has-symbols": "^1.0.3"
},
@ -2886,7 +2898,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@ -3041,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"
@ -3175,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"
},
@ -3367,7 +3388,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -3386,19 +3406,21 @@
}
},
"node_modules/mime-db": {
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
"integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==",
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.27",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
"integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.44.0"
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
@ -4568,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",
@ -4710,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"
@ -6149,7 +6171,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"requires": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
@ -6563,7 +6584,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"requires": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
@ -6595,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",
@ -6832,20 +6852,17 @@
"es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
},
"es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
},
"es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"requires": {
"es-errors": "^1.3.0"
}
@ -6854,7 +6871,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"requires": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
@ -7025,13 +7041,23 @@
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
},
"form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
"integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.35",
"safe-buffer": "^5.2.1"
},
"dependencies": {
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}
}
},
"fs-extra": {
@ -7063,8 +7089,7 @@
"function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
},
"get-caller-file": {
"version": "2.0.5",
@ -7076,7 +7101,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"requires": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
@ -7094,7 +7118,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"requires": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
@ -7185,8 +7208,7 @@
"gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
},
"got": {
"version": "11.8.6",
@ -7246,14 +7268,12 @@
"has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
},
"has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"requires": {
"has-symbols": "^1.0.3"
}
@ -7262,7 +7282,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"requires": {
"function-bind": "^1.1.2"
}
@ -7370,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": {
@ -7457,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"
@ -7613,8 +7632,7 @@
"math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
},
"mime": {
"version": "2.6.0",
@ -7623,16 +7641,16 @@
"dev": true
},
"mime-db": {
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
"integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg=="
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types": {
"version": "2.1.27",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
"integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"requires": {
"mime-db": "1.44.0"
"mime-db": "1.52.0"
}
},
"mimic-fn": {
@ -8150,7 +8168,7 @@
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"form-data": "^2.5.4",
"har-validator": "~5.1.3",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
@ -8508,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",
@ -8627,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": {

View file

@ -20,7 +20,7 @@
"author": "Specter",
"license": "MIT",
"devDependencies": {
"electron": "^39.8.5",
"electron": "^39.8.10",
"electron-builder": "^26.8.1"
},
"build": {
@ -60,5 +60,13 @@
"read-last-lines": "^1.8.0",
"request": "^2.88.2",
"winston": "^3.13.0"
},
"overrides": {
"request": {
"form-data": "^2.5.4"
}
},
"resolutions": {
"**/request/form-data": "^2.5.4"
}
}

View file

@ -709,7 +709,7 @@ colorspace@1.1.x:
color "3.0.x"
text-hex "1.0.x"
combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
combined-stream@^1.0.8, combined-stream@~1.0.6:
version "1.0.8"
resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
@ -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"
@ -1137,6 +1137,18 @@ forever-agent@~0.6.1:
resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz"
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
form-data@^2.5.4, form-data@~2.3.2:
version "2.5.5"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.5.tgz#a5f6364ad7e4e67e95b4a07e2d8c6f711c74f624"
integrity sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
es-set-tostringtag "^2.1.0"
hasown "^2.0.2"
mime-types "^2.1.35"
safe-buffer "^5.2.1"
form-data@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053"
@ -1148,15 +1160,6 @@ form-data@^4.0.5:
hasown "^2.0.2"
mime-types "^2.1.12"
form-data@~2.3.2:
version "2.3.3"
resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz"
integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.6"
mime-types "^2.1.12"
fs-extra@^10.0.0, fs-extra@^10.1.0:
version "10.1.0"
resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz"
@ -1462,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"
@ -1551,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"
@ -1591,7 +1594,7 @@ jsonfile@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz"
integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
dependencies:
optionalDependencies:
graceful-fs "^4.1.6"
jsonfile@^6.0.1:
@ -1711,6 +1714,11 @@ mime-db@1.44.0, mime-db@^1.28.0:
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz"
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12, mime-types@~2.1.19:
version "2.1.27"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz"
@ -1718,6 +1726,13 @@ mime-types@^2.1.12, mime-types@~2.1.19:
dependencies:
mime-db "1.44.0"
mime-types@^2.1.35:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime@^2.5.2:
version "2.6.0"
resolved "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz"
@ -2195,6 +2210,11 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0:
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-buffer@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
safe-stable-stringify@^2.3.1:
version "2.4.3"
resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz"
@ -2452,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"
@ -2512,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"

View file

@ -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

View file

@ -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"]
@ -47,12 +47,13 @@ write_to = "src/cryptoadvance/specter/_version.py"
norecursedirs = "tests/bitcoin* tests/elements* tests/xtestdata_testextensions"
log_format = "[%(levelname)8s] %(message)s %(name)s (%(filename)s:%(lineno)s)"
addopts = "--bitcoind-version v22.0.0 --elementsd-version v0.21.0.2"
addopts = "--bitcoind-version v27.2.0 --elementsd-version v0.21.0.2"
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"
]

View file

@ -1,30 +1,32 @@
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==3.0.10
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
embit==0.6.1
psutil==5.9.0
pyopenssl==24.1.0
pyopenssl==26.0.0
flask_wtf==1.2.1
pgpy==0.6.0
cbor2==5.4.6
cbor2==5.9.0
mnemonic==0.21
cryptography==42.0.7
cryptography==46.0.6
Flask-APScheduler==1.12.4
gunicorn==23.0.0
simple-websocket==0.8.1
protobuf==4.23.3
PyJWT==2.4.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

View file

@ -20,105 +20,145 @@ babel==2.15.0 \
--hash=sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb \
--hash=sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413
# via flask-babel
cbor2==5.4.6 \
--hash=sha256:0b956f19e93ba3180c336282cd1b6665631f2d3a196a9c19b29a833bf979e7a4 \
--hash=sha256:0bd12c54a48949d11f5ffc2fa27f5df1b4754111f5207453e5fae3512ebb3cab \
--hash=sha256:0d2b926b024d3a1549b819bc82fdc387062bbd977b0299dd5fa5e0ea3267b98b \
--hash=sha256:1618d16e310f7ffed141762b0ff5d8bb6b53ad449406115cc465bf04213cefcf \
--hash=sha256:181ac494091d1f9c5bb373cd85514ce1eb967a8cf3ec298e8dfa8878aa823956 \
--hash=sha256:1835536e76ea16e88c934aac5e369ba9f93d495b01e5fa2d93f0b4986b89146d \
--hash=sha256:1c12c0ab78f5bc290b08a79152a8621822415836a86f8f4b50dadba371736fda \
--hash=sha256:24144822f8d2b0156f4cda9427f071f969c18683ffed39663dc86bc0a75ae4dd \
--hash=sha256:309fffbb7f561d67f02095d4b9657b73c9220558701c997e9bfcfbca2696e927 \
--hash=sha256:3316f09a77af85e7772ecfdd693b0f450678a60b1aee641bac319289757e3fa0 \
--hash=sha256:3545b16f9f0d5f34d4c99052829c3726020a07be34c99c250d0df87418f02954 \
--hash=sha256:39452c799453f5bf33281ffc0752c620b8bfa0b7c13070b87d370257a1311976 \
--hash=sha256:3950be57a1698086cf26d8710b4e5a637b65133c5b1f9eec23967d4089d8cfed \
--hash=sha256:456cdff668a50a52fdb8aa6d0742511e43ed46d6a5b463dba80a5a720fa0d320 \
--hash=sha256:4b9f3924da0e460a93b3674c7e71020dd6c9e9f17400a34e52a88c0af2dcd2aa \
--hash=sha256:4bbbdb2e3ef274865dc3f279aae109b5d94f4654aea3c72c479fb37e4a1e7ed7 \
--hash=sha256:4ce1a2c272ba8523a55ea2f1d66e3464e89fa0e37c9a3d786a919fe64e68dbd7 \
--hash=sha256:56dfa030cd3d67e5b6701d3067923f2f61536a8ffb1b45be14775d1e866b59ae \
--hash=sha256:6709d97695205cd08255363b54afa035306d5302b7b5e38308c8ff5a47e60f2a \
--hash=sha256:6e1b5aee920b6a2f737aa12e2b54de3826b09f885a7ce402db84216343368140 \
--hash=sha256:6f9c702bee2954fffdfa3de95a5af1a6b1c5f155e39490353d5654d83bb05bb9 \
--hash=sha256:78304df140b9e13b93bcbb2aecee64c9aaa9f1cadbd45f043b5e7b93cc2f21a2 \
--hash=sha256:79e048e623846d60d735bb350263e8fdd36cb6195d7f1a2b57eacd573d9c0b33 \
--hash=sha256:7bbd3470eb685325398023e335be896b74f61b014896604ed45049a7b7b6d8ac \
--hash=sha256:80ac8ba450c7a41c5afe5f7e503d3092442ed75393e1de162b0bf0d97edf7c7f \
--hash=sha256:9394ca49ecdf0957924e45d09a4026482d184a465a047f60c4044eb464c43de9 \
--hash=sha256:94f844d0e232aca061a86dd6ff191e47ba0389ddd34acb784ad9a41594dc99a4 \
--hash=sha256:96087fa5336ebfc94465c0768cd5de0fcf9af3840d2cf0ce32f5767855f1a293 \
--hash=sha256:b893500db0fe033e570c3adc956af6eefc57e280026bd2d86fd53da9f1e594d7 \
--hash=sha256:c285a2cb2c04004bfead93df89d92a0cef1874ad337d0cb5ea53c2c31e97bfdb \
--hash=sha256:d2984a488f350aee1d54fa9cb8c6a3c1f1f5b268abbc91161e47185de4d829f3 \
--hash=sha256:d54bd840b4fe34f097b8665fc0692c7dd175349e53976be6c5de4433b970daa4 \
--hash=sha256:db9eb582fce972f0fa429d8159b7891ff8deccb7affc4995090afc61ce0d328a \
--hash=sha256:e5094562dfe3e5583202b93ef7ca5082c2ba5571accb2c4412d27b7d0ba8a563 \
--hash=sha256:e73ca40dd3c7210ff776acff9869ddc9ff67bae7c425b58e5715dcf55275163f \
--hash=sha256:ff95b33e5482313a74648ca3620c9328e9f30ecfa034df040b828e476597d352
cbor2==5.9.0 \
--hash=sha256:0322296b9d52f55880e300ba8ba09ecf644303b99b51138bbb1c0fb644fa7c3e \
--hash=sha256:0485d3372fc832c5e16d4eb45fa1a20fc53e806e6c29a1d2b0d3e176cedd52b9 \
--hash=sha256:08388ea54195738602b4c4999966bcaef6f0b17d293c9658658409d9fff96f57 \
--hash=sha256:1d02b65f070fd726bdc310d927228975bb655d155bf059b6eb7cacefb3dca86f \
--hash=sha256:1da96ce5d852fe3d342c1eb2c202a52d1c97edfddc9230f1be7e02674662bf26 \
--hash=sha256:1f223dffb1bcdd2764665f04c1152943d9daa4bc124a576cd8dee1cad4264313 \
--hash=sha256:23606d31ba1368bd1b6602e3020ee88fe9523ca80e8630faf6b2fc904fd84560 \
--hash=sha256:2372d357d403e7912f104ff085950ffc82a5854d6d717f1ca1ce16a40a0ef5a7 \
--hash=sha256:25bec7beb2089465382b1be72e78667fe9090598800826559c3e3008cf0db743 \
--hash=sha256:27695cbd70c90b8de5c4a284642c2836449b14e2c2e07e3ffe0744cb7669a01b \
--hash=sha256:2a54fbb32cb828c214f7f333a707e4aec61182e7efdc06ea5d9596d3ecee624a \
--hash=sha256:3095dc49e75572841a9534cbfdabc2a17487ea4ee33341436abc4a7ac7245a3a \
--hash=sha256:34a6cb15e6ab6a8eae94ad2041731cd3ef786af43a8df99f847969af5b902ee7 \
--hash=sha256:380e534482b843e43442b87d8777a7bf9bed20cb7526f89b780c3400f617304b \
--hash=sha256:420d2490c7836c81151b4bd591c35cffc55391e33e7e333c50fda391bcea7d31 \
--hash=sha256:422817286c1d0ce947fb2f7eca9212b39bddd7231e8b452e2d2cc52f15332dba \
--hash=sha256:4753a6d1bc71054d9179557bc65740860f185095ccb401d46637fff028a5b3ec \
--hash=sha256:4aa07b392cc3d76fb31c08a46a226b58c320d1c172ff3073e864409ced7bc50f \
--hash=sha256:4cd43d8fc374b31643b2830910f28177a606a7bc84975a62675dd3f2e320fc7b \
--hash=sha256:5326336f633cc89dfe543c78829c16c3a6449c2c03277d1ddba99086c3323363 \
--hash=sha256:53cfa49e0df9c639beb871d480de098eedc81eb63ff29f2dc922720d7577b676 \
--hash=sha256:55bea0dd9a7d354e35f4e5fe58ceab393e76962713749dc3a0a64a0e5d19545e \
--hash=sha256:5e702b02d42a5ace45425b595ffe70fe35aebaf9a3cdfdc2c758b6189c744422 \
--hash=sha256:65f8eac3268c608533f326f0fd9010ab1b2a8a917b05edaf3853116336821669 \
--hash=sha256:7221483fad0c63afa4244624d552abf89d7dfdbc5f5edfc56fc1ff2b4b818975 \
--hash=sha256:7d1ddc4541e7367ac58c2470cc0df847f7137167fe4f5729e2d3cc0b993d7da4 \
--hash=sha256:837754ece9052b3f607047e1741e5f852a538aa2b0ee3db11c82a8fa11804aa4 \
--hash=sha256:85c7a46279ac8f226e1059275221e6b3d0e370d2bb6bd0500f9780781615bcea \
--hash=sha256:86baf870d4c0bfc6f79de3801f3860a84ab76d9c8b0abb7f081f2c14c38d79d3 \
--hash=sha256:971d425b3a23b75953d8853d5f9911bdeefa09d759ee3b5e6b07b5ff3cbd9073 \
--hash=sha256:9a4907e0c3035bb8836116854ed8e56d8aef23909d601fa59706320897ec2551 \
--hash=sha256:a9d6e4e0f988b0e766509a8071975a8ee99f930e14a524620bf38083106158d2 \
--hash=sha256:ac684fe195c39821fca70d18afbf748f728aefbfbf88456018d299e559b8cae0 \
--hash=sha256:ae6c706ac1d85a0b3cb3395308fd0c4d55e3202b4760773675957e93cdff45fc \
--hash=sha256:cc5efec69055c3c470997935d95762be7e4bfd1248d88fb1a33bb7e0f45712e9 \
--hash=sha256:d1a21c006760f95acd9509cc5a7d15d6fc82e58f721f94fa9039b4e77189a6e5 \
--hash=sha256:d8524a8c142c3cc228e635f8a97499a6c0b18ca91382e8276565658035cdcb6d \
--hash=sha256:dcf0f695873e5c94bd072d6af8698e72b8fb7f7a18f37e0bced1041b7111a6cf \
--hash=sha256:f29e5c3abcc91c1aeefecde0e057bf33f1655588d3065c6560c30ceb3be6f333 \
--hash=sha256:f797532d13469f2193e5c16e827d8df7a8c33674b19be755790b54ab231e6a73 \
--hash=sha256:f7c9751a9611601ab326d8f5837f01379195bbf06175fb4effeb552140e7c9e8 \
--hash=sha256:fb7afe77f8d269e42d7c4b515c6fd14f1ccc0625379fb6829b269f493d16eddd \
--hash=sha256:fbb06f34aa645b4deca66643bba3d400d20c15312d1fe88d429be60c1ab50f27 \
--hash=sha256:fbdcf4d74acbeb7672e6413e81cd2c1ced1a4a8cf949484ac54e9af5265c3c72
# 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
cffi==1.16.0 \
--hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \
--hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \
--hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \
--hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \
--hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \
--hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \
--hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \
--hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \
--hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \
--hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \
--hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \
--hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \
--hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \
--hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \
--hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \
--hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \
--hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \
--hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \
--hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \
--hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \
--hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \
--hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \
--hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \
--hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \
--hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \
--hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \
--hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \
--hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \
--hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \
--hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \
--hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \
--hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \
--hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \
--hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \
--hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \
--hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \
--hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \
--hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \
--hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \
--hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \
--hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \
--hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \
--hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \
--hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \
--hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \
--hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \
--hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \
--hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \
--hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \
--hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \
--hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \
--hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357
cffi==2.0.0 \
--hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \
--hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \
--hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \
--hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \
--hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \
--hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \
--hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \
--hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \
--hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \
--hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \
--hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \
--hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \
--hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \
--hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \
--hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \
--hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \
--hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \
--hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \
--hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \
--hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \
--hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \
--hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \
--hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \
--hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \
--hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \
--hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \
--hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \
--hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \
--hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \
--hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \
--hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \
--hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \
--hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \
--hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \
--hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \
--hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \
--hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \
--hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \
--hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \
--hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \
--hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \
--hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \
--hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \
--hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \
--hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \
--hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \
--hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \
--hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \
--hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \
--hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \
--hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \
--hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \
--hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \
--hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \
--hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \
--hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \
--hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \
--hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \
--hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \
--hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \
--hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \
--hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \
--hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \
--hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \
--hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \
--hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \
--hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \
--hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \
--hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \
--hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \
--hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \
--hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \
--hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \
--hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \
--hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \
--hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \
--hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \
--hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \
--hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \
--hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \
--hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \
--hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \
--hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \
--hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf
# via cryptography
charset-normalizer==2.0.12 \
--hash=sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597 \
@ -134,51 +174,68 @@ 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==42.0.7 \
--hash=sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55 \
--hash=sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785 \
--hash=sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b \
--hash=sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886 \
--hash=sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82 \
--hash=sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1 \
--hash=sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda \
--hash=sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f \
--hash=sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68 \
--hash=sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60 \
--hash=sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7 \
--hash=sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd \
--hash=sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582 \
--hash=sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc \
--hash=sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858 \
--hash=sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b \
--hash=sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2 \
--hash=sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678 \
--hash=sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13 \
--hash=sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4 \
--hash=sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8 \
--hash=sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604 \
--hash=sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477 \
--hash=sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e \
--hash=sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a \
--hash=sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9 \
--hash=sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14 \
--hash=sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda \
--hash=sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da \
--hash=sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562 \
--hash=sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2 \
--hash=sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9
cryptography==46.0.6 \
--hash=sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70 \
--hash=sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d \
--hash=sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a \
--hash=sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0 \
--hash=sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97 \
--hash=sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30 \
--hash=sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759 \
--hash=sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c \
--hash=sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead \
--hash=sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275 \
--hash=sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58 \
--hash=sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f \
--hash=sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361 \
--hash=sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507 \
--hash=sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa \
--hash=sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b \
--hash=sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b \
--hash=sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8 \
--hash=sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8 \
--hash=sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72 \
--hash=sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175 \
--hash=sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e \
--hash=sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124 \
--hash=sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a \
--hash=sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c \
--hash=sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f \
--hash=sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d \
--hash=sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4 \
--hash=sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c \
--hash=sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290 \
--hash=sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca \
--hash=sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d \
--hash=sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a \
--hash=sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed \
--hash=sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a \
--hash=sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb \
--hash=sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8 \
--hash=sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707 \
--hash=sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410 \
--hash=sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736 \
--hash=sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2 \
--hash=sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4 \
--hash=sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013 \
--hash=sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19 \
--hash=sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b \
--hash=sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738 \
--hash=sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463 \
--hash=sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77 \
--hash=sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4
# via
# -r requirements.in
# noiseprotocol
# pgpy
# pyopenssl
ecdsa==0.19.0 \
--hash=sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a \
--hash=sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8
ecdsa==0.19.2 \
--hash=sha256:62635b0ac1ca2e027f82122b5b81cb706edc38cd91c63dda28e4f3455a2bf930 \
--hash=sha256:840f5dc5e375c68f36c1a7a5b9caad28f95daa65185c9253c0c08dd952bb7399
# via hwi
embit==0.6.1 \
--hash=sha256:16a84c6668dc9ffc907594457a46f7142cee379646bc009a5a9b77b0d2cb4e12
@ -206,13 +263,13 @@ flask-babel==3.1.0 \
--hash=sha256:be015772c5d7f046f3b99c508dcf618636eb93d21b713b356db79f3e79f69f39 \
--hash=sha256:deb3ee272d5adf97f5974ed09ab501243d63e7fb4a047501a00de4bd4aca4830
# via -r requirements.in
flask-cors==3.0.10 \
--hash=sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438 \
--hash=sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de
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 \
@ -295,14 +352,16 @@ 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
# via -r requirements.in
h11==0.14.0 \
--hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \
--hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
h11==0.16.0 \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
# via wsproto
hidapi==0.14.0 \
--hash=sha256:01929fbbe206ebcb0bad9b8e925e16de0aa8f872bf80a263f599e519866d9900 \
@ -375,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 \
@ -389,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
@ -514,6 +573,10 @@ numpy==1.26.4 \
--hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \
--hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f
# via pandas
packaging==26.0 \
--hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \
--hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529
# via gunicorn
pandas==2.2.2 \
--hash=sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863 \
--hash=sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2 \
@ -569,10 +632,6 @@ protobuf==4.23.3 \
# via
# -r requirements.in
# hwi
packaging==26.0 \
--hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 \
--hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4
# via gunicorn
psutil==5.9.0 \
--hash=sha256:072664401ae6e7c1bfb878c65d7282d4b4391f1bc9a56d5e03b5a490403271b5 \
--hash=sha256:1070a9b287846a21a5d572d6dddd369517510b68710fca56b0e9e02fd24bed9a \
@ -685,21 +744,21 @@ psycopg2-binary==2.9.5 \
pyaes==1.6.1 \
--hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
# via hwi
pyasn1==0.6.0 \
--hash=sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c \
--hash=sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473
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.4.0 \
--hash=sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf \
--hash=sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba
pyjwt==2.13.0 \
--hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \
--hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728
# via -r requirements.in
pyopenssl==24.1.0 \
--hash=sha256:17ed5be5936449c5418d1cd269a1a9e9081bc54c17aed272b45856a3d3dc86ad \
--hash=sha256:cabed4bfaa5df9f1a16c0ef64a0cb65318b5cd077a7eda7d6970131ca2f41a6f
pyopenssl==26.0.0 \
--hash=sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81 \
--hash=sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc
# via -r requirements.in
pyserial==3.5 \
--hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \
@ -718,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 \
@ -734,9 +793,9 @@ pytz==2024.1 \
# flask-babel
# flask-restful
# pandas
requests==2.31.0 \
--hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 \
--hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f
requests==2.33.0 \
--hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \
--hash=sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652
# via
# -r requirements.in
# cryptoadvance-spectrum
@ -755,7 +814,6 @@ six==1.16.0 \
# -r requirements.in
# apscheduler
# ecdsa
# flask-cors
# flask-restful
# python-dateutil
specterext-exfund==0.1.7 \
@ -768,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 \
@ -829,10 +887,14 @@ tenacity==8.3.0 \
--hash=sha256:3649f6443dbc0d9b01b9d8020a9c4ec7a1ff5f6f3c6c8a036ef371f573fe9185 \
--hash=sha256:953d4e6ad24357bceffbc9707bc74349aca9d245f68eb65419cf0c249a1949a2
# via plotly
typing-extensions==4.11.0 \
--hash=sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0 \
--hash=sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a
# via hwi
typing-extensions==4.15.0 \
--hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
# via
# cryptography
# hwi
# pyjwt
# pyopenssl
tzdata==2024.1 \
--hash=sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd \
--hash=sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252
@ -841,15 +903,17 @@ tzlocal==5.2 \
--hash=sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8 \
--hash=sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e
# via apscheduler
urllib3==1.26.18 \
--hash=sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07 \
--hash=sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0
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
wsproto==1.2.0 \
--hash=sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065 \
@ -861,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

View file

@ -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

View file

@ -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

View file

@ -65,13 +65,17 @@ class BitcoinCore(Device):
def taproot_available(self, rpc):
try:
# currently only master branch supports tr() descriptors
# TODO: replace to 220000
core_version = rpc.getnetworkinfo().get("version", 0)
info = rpc.getblockchaininfo()
taproot_active = (core_version >= 219900) and (
info.get("softforks", {}).get("taproot", {}).get("active", False)
)
if core_version >= 240000:
# Core 24 moved softfork status from getblockchaininfo.softforks
# to the getdeploymentinfo RPC's deployments field.
deployments = rpc.getdeploymentinfo().get("deployments", {})
taproot_active = deployments.get("taproot", {}).get("active", False)
else:
softforks = rpc.getblockchaininfo().get("softforks", {})
taproot_active = (core_version >= 219900) and softforks.get(
"taproot", {}
).get("active", False)
taproot_support = self.use_descriptors(rpc) and taproot_active
self.taproot_support = taproot_support
return taproot_support

View file

@ -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"]

View file

@ -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 ...")

View file

@ -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:

View file

@ -1100,6 +1100,10 @@ input[type="number"]::-webkit-outer-spin-button,
z-index: 50;
}
.z-20 {
z-index: 20;
}
.z-30 {
z-index: 30;
}

View file

@ -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>

View file

@ -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") }}

View file

@ -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>

View file

@ -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

View file

@ -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!
"""

View file

@ -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(

View file

@ -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:
"""

View file

@ -1,8 +1,8 @@
# Trusted reference: verbatim plaintext of https://bitcoincore.org/bin/bitcoin-core-22.0/SHA256SUMS
# Trusted reference: verbatim plaintext of https://bitcoincore.org/bin/bitcoin-core-27.2/SHA256SUMS
# Provenance:
# Source URL: https://bitcoincore.org/bin/bitcoin-core-22.0/SHA256SUMS
# Signature URL: https://bitcoincore.org/bin/bitcoin-core-22.0/SHA256SUMS.asc
# Verified on: 2026-04-15
# Source URL: https://bitcoincore.org/bin/bitcoin-core-27.2/SHA256SUMS
# Signature URL: https://bitcoincore.org/bin/bitcoin-core-27.2/SHA256SUMS.asc
# Verified on: 2026-04-19
# Verified by: Good signatures from:
# - fanquake (primary fingerprint E777299FC265DD04793070EB944D35F9AC3DB76A,
# signing subkey CFB16E21C950F67FA95E558F2EEB9F5CC09526C1)
@ -10,26 +10,30 @@
# Builder keys sourced from: https://github.com/bitcoin-core/guix.sigs/tree/main/builder-keys
# Do not edit the hash lines below — they are the on-disk trust anchor compared
# against downloaded tarballs by tests/install_noded.sh::verify_binary.
9547fa03574f8bde296f707c7d9f7d89827c75c5a28f84402578a4fa92a787ec bitcoin-22.0-aarch64-linux-gnu-debug.tar.gz
ac718fed08570a81b3587587872ad85a25173afa5f9fbbd0c03ba4d1714cfa3e bitcoin-22.0-aarch64-linux-gnu.tar.gz
80071e0ecd24edfec8a1972b495b9822c79a5d33c7123bff51688638aac97cab bitcoin-22.0-arm-linux-gnueabihf-debug.tar.gz
b8713c6c5f03f5258b54e9f436e2ed6d85449aa24c2c9972f91963d413e86311 bitcoin-22.0-arm-linux-gnueabihf.tar.gz
8f70852feb39078e02182563517d17bdfc4a12904cf1bdabbae95594d9a1e473 bitcoin-22.0-codesignatures-22.0.tar.gz
d0e9d089b57048b1555efa7cd5a63a7ed042482045f6f33402b1df425bf9613b bitcoin-22.0.tar.gz
bfc04a3c4e8b613bfd9359e54da6cc60f027860e9723f9a6bfd6f13873eb811f bitcoin-22.0-powerpc64-linux-gnu-debug.tar.gz
2cca5f99007d060aca9d8c7cbd035dfe2f040dd8200b210ce32cdf858479f70d bitcoin-22.0-powerpc64-linux-gnu.tar.gz
5f0bf1491bc8825ca1506f7cf586030f06bb17a563ccde92e8c75720022704e6 bitcoin-22.0-powerpc64le-linux-gnu-debug.tar.gz
91b1e012975c5a363b5b5fcc81b5b7495e86ff703ec8262d4b9afcfec633c30d bitcoin-22.0-powerpc64le-linux-gnu.tar.gz
59b16e63aa935f50fd2813efe7f137187fcf0fff84e3205a9c6cb462a8bb160c bitcoin-22.0-riscv64-linux-gnu-debug.tar.gz
9cc3a62c469fe57e11485fdd32c916f10ce7a2899299855a2e479256ff49ff3c bitcoin-22.0-riscv64-linux-gnu.tar.gz
3b3e2680f7d9304c13bfebaf6445ada40d72324b4b3e0a07de9db807389a6c5b bitcoin-22.0-osx-signed.dmg
52449aa894a6ce5653315e1260d0ce87c1d9f490afe3c92b44285710804b11ae bitcoin-22.0-osx-unsigned.dmg
f51156774c24c0ac5cc30237fa08aa17ed04a180dfd72c3e7d20fdc3f45806dc bitcoin-22.0-osx-unsigned.tar.gz
2744d199c3343b2d94faffdfb2c94d75a630ba27301a70e47b0ad30a7e0155e9 bitcoin-22.0-osx64.tar.gz
3a4f05657c048d3e9505bdb9c4fb3658e5e3d4233b0b93c1853e080620589765 bitcoin-22.0-x86_64-linux-gnu-debug.tar.gz
59ebd25dd82a51638b7a6bb914586201e67db67b919b2a1ff08925a7936d1b16 bitcoin-22.0-x86_64-linux-gnu.tar.gz
9169989d649937c0f9ebccd3ab088501328aa319fe9e91fc7ea8e8cf0fcccede bitcoin-22.0-win64-setup.exe
f890473d6d910d478f8ff08f9356d0305d19b46cf06e4fc3b5a49b0b684fd2a7 bitcoin-22.0-win-unsigned.tar.gz
0a97ebc8ae44913e3ef9c5b1ddd2af3a4ffb0ba25b6ab1ee8173e40e60499402 bitcoin-22.0-win64-debug.zip
ecc579d006230d6ffc5a5b7b53ce8c76477d37c1c7bad69694e9c2d69f00331d bitcoin-22.0-win64-setup-unsigned.exe
9485e4b52ed6cebfe474ab4d7d0c1be6d0bb879ba7246a8239326b2230a77eb1 bitcoin-22.0-win64.zip
eb0c8518ad2e96d01208f589ecd827b5a951be6dfcac418d4b807842a6ebdf65 bitcoin-27.2-aarch64-linux-gnu-debug.tar.gz
154c9b9e6e17136edc8f20fda5d252fb339e727e4a85ef49e7d8facb9085f2d3 bitcoin-27.2-aarch64-linux-gnu.tar.gz
4a05d46dcb74d337f62b78b884df080534677897fd9e1a0ae41253547789cef8 bitcoin-27.2-arm-linux-gnueabihf-debug.tar.gz
fb00da13525b52a4aad091c6521f94a79879e5ca0956c2302289301f71e6303c bitcoin-27.2-arm-linux-gnueabihf.tar.gz
3a4795d591c0e506e88ce227b17048d347be37611ba98837a71a16c3a5f908c6 bitcoin-27.2-arm64-apple-darwin.zip
4e91003a95108f85e85f92c3af23aa6108175a1d0a0c60cb515c2d3daf8a20f4 bitcoin-27.2-arm64-apple-darwin-unsigned.tar.gz
48800ea29baca2e27f1544fc0c4d4cd096a908291c1e681bd9daa632645bce06 bitcoin-27.2-arm64-apple-darwin-unsigned.zip
8f2247f4786f3559d37189b58452c91623efc5fa6886c975fa9386f9ff3f1001 bitcoin-27.2-arm64-apple-darwin.tar.gz
33b59d57b1a3f11ca9c7993891a359fdc19e62284be3235d3951053cbbe81a60 bitcoin-27.2-codesignatures-27.2.tar.gz
5a8b0094b3c6bc7f63c7fd6e0ba2e733a3022f09c0064c96ccd6e331bd7b9f6a bitcoin-27.2.tar.gz
6a273a634a04123c48c4a4be9ddbeeba27d2acdbefecd1025cfca1f30b394759 bitcoin-27.2-powerpc64-linux-gnu-debug.tar.gz
566ed37e33ce529c75d9d0c72ff3894d0d5a311bd5f21be2de7495937348e454 bitcoin-27.2-powerpc64-linux-gnu.tar.gz
6f9824a833a0c9a2c11cabf302cf0b0f573471cf8a7f0f7ad8723db4094515d5 bitcoin-27.2-powerpc64le-linux-gnu-debug.tar.gz
efb66cc2648b3968f7186c990640589bd6fd72d93879b0cc1ff2adef9cd377b9 bitcoin-27.2-powerpc64le-linux-gnu.tar.gz
fdc90b4692a5aaf04f6626b71fe40368ec0bcf000d066b1efdab99318000a3a8 bitcoin-27.2-riscv64-linux-gnu-debug.tar.gz
0c272ec7b7bb6bdee410fb177ceaab0df0373de5aed2228ab9fb4e6128ccc793 bitcoin-27.2-riscv64-linux-gnu.tar.gz
d74feac2e7bbb7df84c8f67ca6216ef4f83eb7e9c2213a901896d7814bfafb90 bitcoin-27.2-x86_64-apple-darwin.zip
cbac742ce80dac1b57d455680b3accb8f75c20e782169185ef519b3e0534d498 bitcoin-27.2-x86_64-apple-darwin-unsigned.tar.gz
8630d96eba03ecb57066442947cc08d58367666f96d825c636015ff458f46c16 bitcoin-27.2-x86_64-apple-darwin-unsigned.zip
6ebc56ca1397615d5a6df2b5cf6727b768e3dcac320c2d5c2f321dcaabc7efa2 bitcoin-27.2-x86_64-apple-darwin.tar.gz
8eb4e52ab1b3a27640a2c1a00c526815ab8a740443f963f5e6dfc5fdb37bcc2c bitcoin-27.2-x86_64-linux-gnu-debug.tar.gz
acc223af46c178064c132b235392476f66d486453ddbd6bca6f1f8411547da78 bitcoin-27.2-x86_64-linux-gnu.tar.gz
30e9a0e4b3e1e32aceb4773615c0a9ed54443b037ef541ea3be07508377cf128 bitcoin-27.2-win64-setup.exe
723a70ca4c0f7d0cef028d6cf502f595e3684c9f30c6c3c6b88d05c26b638c35 bitcoin-27.2-win64-debug.zip
5f779dbf911e75c4ca4a57b04f972a52006ed49de3519c640fef99a596b22e0e bitcoin-27.2-win64-setup-unsigned.exe
e92e8531f50915a74790689e6143d8d712b471233c38231f60bfa40ffb820bca bitcoin-27.2-win64-unsigned.tar.gz
82e18f768aa5962b3c002d7f5d6ec9338896804f48406af4b5054c927575dbdf bitcoin-27.2-win64.zip

View file

@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

15
tests/fixtures/jade_seedqr_abandon.txt vendored Normal file
View file

@ -0,0 +1,15 @@
█████████████████████████████
██ ▄▄▄▄▄ ██▄▀▀ ▀▄▀▄█ ▄▄▄▄▄ ██
██ █   █ █▄  ▀▄▀▄▀▄█ █   █ ██
██ █▄▄▄█ ██▀▄██▀▄▀▄█ █▄▄▄█ ██
██▄▄▄▄▄▄▄█ ▀ █ ▀▄▀▄█▄▄▄▄▄▄▄██
██  ▄▀ ▄▄▄▄█  █▀▄▀▄ ▄▀█▀▄▀███
██ ▀██▄▀▄▀▀▄█▀██▄▀▄▀▄▀▄▀▄▀▄██
████▀▄▄▄▄▀▀██▀ ▀█▀▄▀▄▀▄▀▄▀▄██
██▄▀█ ██▄▀██ ▄ ▀▄▀▄▀▄▀▄▀▄▀▄██
██▄██▄▄▄▄▄▀▀▀ ██▄▀ ▄▄▄ ▀▄▀▄██
██ ▄▄▄▄▄ █ ▄ ▀█▄█▀ █▄█ ▀▄▀▄██
██ █   █ █▄▄ ▀ ▀█▀▄ ▄ ▄▀▄ ███
██ █▄▄▄█ █ ▀█▄ ▀ ▀▄▀▄▀▄▀▄█▄██
██▄▄▄▄▄▄▄█▄█▄▄██▄█▄█▄█▄█▄█▄██
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

View file

@ -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

View file

@ -181,7 +181,11 @@ def test_addressinfo(caplog, client, funded_ghost_machine_wallet):
assert res.data.decode().startswith(
'{"error":"Request error for method getaddressinfo'
)
assert res.data.decode().endswith('Invalid address format"}\n')
# Core 22 says "Invalid address format"; Core 24+ says "Invalid checksum".
assert (
'Invalid address format"}\n' in res.data.decode()
or 'Invalid checksum"}\n' in res.data.decode()
)
# send post request with address, not belonging to wallet
# this recreates an edge case, see https://github.com/cryptoadvance/specter-desktop/issues/2000

180
tests/test_hwi_server.py Normal file
View 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
View 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"

View file

@ -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

View file

@ -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",
}

View file

@ -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")

View file

@ -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):

View file

@ -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

View 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

View file

@ -347,7 +347,10 @@ def test_WalletAwareTxItem(bitcoin_regtest, parent_mock, empty_data_folder):
print("\n\nOutgoing-Transaction (0.2 btc)")
print("=========================================")
txid_outgoing_addr = "n4MN27Lk7Yh3pwfjCiAbRXtRVjs4Uk67fG"
# bech32 destination so Core keeps change as wpkh (Core 24+ matches change
# type to destination type; legacy destinations produce legacy change that
# the wpkh-only watch descriptor in this test does not recognise).
txid_outgoing_addr = "bcrt1qvtdx75y4554ngrq6aff3xdqnvjhmct5wck95qs"
print(f"address = {txid_outgoing_addr}")
txid_outgoing = wrpc.sendtoaddress(txid_outgoing_addr, 0.2)
print(f"balance: {wrpc.getbalances()['mine']['trusted']}")

View file

@ -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"

View file

@ -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 = []

View file

@ -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

2469
yarn.lock

File diff suppressed because it is too large Load diff