Feature: Api framework (#1232)

* Included API end points

Included 3 API end points:

###/api/specter ["GET"]
Basic Specter info from app.specter class

###/api/full_txlist/ ["GET"]
Full Transaction list for all wallets

###"/api/wallet_info/<wallet_alias>/ ["GET"]
More detailed wallet info and transactions from the app.specter.wallet_manager class

* Update src/cryptoadvance/specter/controller.py

Include `wallet.check_unused()`

Co-authored-by: benk10 <ben.kaufman10@gmail.com>

* API Update

Made a few changes:
- Flattened the wallet list to be similar to full transaction list
- included versioning on url `/v1alpha/`
- Included `wallet.utxo` data
- Improved `safe_serialize` for better json output

Pending Updates:
- Create a separate file for API (as a blueprint)
- Explore using Flask-restful for improved authentication (downside is that it is another dependency - not sure if already a requirement for specter)

* adding a rest-api

* Implemented API end points inside of example.py

* Merging with remote

* Fixed API.md

* Removing example-resources

* migrating to focal due to cryptography needs newer pip

* fix the image

* black and tidy up

* Deleted more useless stuff

* first take for psbt (still broken)

* get tests to work

* fix devices_signed undefined

* fix part2

* fix part 3

* fix part 4

* Rest Get-Requests for psbt working

* fix part 5

* extending psbt_creator with json format

* tests finished. bitcoind_regtest changed to scope session

* speed up tests

* fix tests

* tiny test for LiquidRpc, probes improvements

* try fixing testing-infra

* kick

* finally fix infra, hopefully

* removing assertion

* refactoring and fixing API

* fix the rest-tests

* fix last liquid failing test

* polishing: error_handling via decorator

* split-up docs

* fix requirements.in

* polishing REST-API

* optimize startup for internal_nodes

* Update src/cryptoadvance/specter/internal_node.py

Co-authored-by: benk10 <ben.kaufman10@gmail.com>

Co-authored-by: Alpha Zeta <40473443+pxsocs@users.noreply.github.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: Alpha Zeta <alphazeta@protonmail.com>
Co-authored-by: Stepan Snigirev <snigirev.stepan@gmail.com>
This commit is contained in:
Kim Neunert 2021-07-08 20:10:14 +02:00 committed by GitHub
parent 92b842525b
commit e10cea0456
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 2614 additions and 693 deletions

View file

@ -61,7 +61,7 @@ test_task:
cypress_test_task:
container:
image: registry.gitlab.com/cryptoadvance/specter-desktop/cypress-python
image: registry.gitlab.com/cryptoadvance/specter-desktop/cypress-python:focal
pre_prep_script:
- apt-get update && apt-get install -y --no-install-recommends python3-dev python3-pip python3-virtualenv bc
# The stupid old debian-package is not installing a proper binary but just the python-package

11
.dockerignore Normal file
View file

@ -0,0 +1,11 @@
.env
pyinstaller
.git
release
docs
.vscode
tests
docker
.pytest_cache
build
dist

3
.gitignore vendored
View file

@ -31,5 +31,4 @@ elmd-conn.json
tests/bitcoin
tests/bitcoin.binary
tests/bitcoin.compile
bla
blub
token.sh

View file

@ -121,6 +121,9 @@ pytest --docker
Running specific test subsets:
```
# Run all tests but not the slow ones
pytest -m "not slow"
# Run all the tests in a specific test file
pytest tests/test_specter.py
@ -129,6 +132,9 @@ pytest tests/test_specter.py -k Manager
# Run a specific test
pytest tests/test_specter.py::test_specter
# Run tests and show the fixture-setup and usage
pytest --setup-show
```
Check the cypress-section on how to run cypress-frontend-tests.

View file

@ -0,0 +1,62 @@
FROM ubuntu:20.04
RUN apt-get update && \
apt-get install -y apt-transport-https curl
RUN curl -sL https://deb.nodesource.com/setup_12.x -o nodesource_setup.sh
RUN bash nodesource_setup.sh
RUN apt-get install -y nodejs
# Install latest NPM and Yarn
RUN npm install -g npm@latest
RUN npm install -g yarn@latest
# install additional native dependencies build tools
RUN apt install -y build-essential
# install Git client
RUN apt-get install -y git
# install unzip utility to speed up Cypress unzips
# https://github.com/cypress-io/cypress/releases/tag/v3.8.0
RUN apt-get install -y unzip
# avoid any prompts
ENV DEBIAN_FRONTEND noninteractive
#install tzdata package
RUN apt-get install -y tzdata
# set your timezone
RUN ln -fs /usr/share/zoneinfo/America/New_York /etc/localtime
RUN dpkg-reconfigure --frontend noninteractive tzdata
# install Cypress dependencies (separate commands to avoid time outs)
RUN apt-get install -y \
libgtk2.0-0
RUN apt-get install -y \
libnotify-dev
RUN apt-get install -y \
libgconf-2-4 \
libnss3 \
libxss1
RUN apt-get install -y \
libasound2 \
xvfb
# a few environment variables to make NPM installs easier
# good colors for most applications
ENV TERM xterm
# avoid million NPM install messages
ENV npm_config_loglevel warn
# allow installing when the main user is root
ENV npm_config_unsafe_perm true
# versions of local tools
RUN echo " node version: $(node -v) \n" \
"npm version: $(npm -v) \n" \
"yarn version: $(yarn -v) \n" \
"debian version: $(cat /etc/debian_version) \n" \
"user: $(whoami) \n" \
"git: $(git --version) \n"
RUN echo "More version info"
RUN cat /etc/lsb-release
RUN cat /etc/os-release

View file

@ -0,0 +1,11 @@
A base-image used in cypress-python to use a newer ubuntu focal rather than a buster.
Create it like this:
```
docker build . -t registry.gitlab.com/cryptoadvance/specter-desktop/cypress-base-ubuntu-focal:latest
docker push registry.gitlab.com/cryptoadvance/specter-desktop/cypress-base-ubuntu-focal:latest
```
used in cypress-python

View file

@ -1,15 +1,11 @@
FROM cypress/base:12
FROM registry.gitlab.com/cryptoadvance/specter-desktop/cypress-base-ubuntu-focal:latest
RUN apt-get update && apt-get install -y \
python3-pip python3-virtualenv zip unzip file apt libusb-1.0-0-dev libudev-dev \
bc libevent-2.1-6
bc libevent-2.1-7
# Stuff needed for Elements (compilation)
RUN DEBIAN_FRONTEND="noninteractive" apt-get install --no-install-recommends -y bsdmainutils libboost-test-dev libboost-filesystem-dev libboost-thread-dev libsqlite3-dev git
# Tor is dependent on /usr/lib/x86_64-linux-gnu/libevent-2.1.so.7 which is not available
# on this buster based image. So here is tiny ugly hack which make it working
RUN ln -s /usr/lib/x86_64-linux-gnu/libevent-2.1.so.6.0.2 /usr/lib/x86_64-linux-gnu/libevent-2.1.so.7
RUN DEBIAN_FRONTEND="noninteractive" apt-get install --no-install-recommends -y bsdmainutils libboost-test-dev libboost-filesystem-dev libboost-thread-dev libsqlite3-dev git libevent-pthreads-2.1-7
WORKDIR /test

36
docs/api/README.md Normal file
View file

@ -0,0 +1,36 @@
# Specter API
Specter provides a Rest-API which is, by default, in production deactivated. In order to activate, you need to export a variable like that:
```
export SPECTER_API_ACTIVE=True
```
The Authentication is also necessary if you don't activate any Authentication mechanism.
In order to make reasonable assumptions about how stable a specific endpoint is, we're versioning them via the URL. Currently, all endpoints are preset with `v1alpha` which pretty much don't give you any guarantee.
# Basic Usage
Curl:
```bash
curl -u admin:secret -X GET http://127.0.0.1:25441/api/v1alpha/specter | jq .
```
Python:
```python
import requests
response = requests.get('http://127.0.0.1:25441/api/v1alpha/specter', auth=('admin', 'secret'))
json.loads(response.text)
```
# Endpoints
* [Liveness](./ep_liveness.md): Is specter up and running?
* [Readyness](./ep_readyness.md): Is specter ready to serve requests?
* [Specter](./ep_specter.md): Get details about the instance
* [Specter Full Tx List](./ep_specter_fulltxlist.md): Gives a full tx_list of all transactions.
* [Wallet](./ep_wallets_wallet.md): Details about a specific Wallet
* [Wallet PSBT](./ep_wallets_psbt.md): Listing and creating PSBTs

24
docs/api/ep_liveness.md Normal file
View file

@ -0,0 +1,24 @@
## Liveness
This endpoint works as healthz-check. See e.g. here:
https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
**URL** : `/api/healthz/liveness`
**Method** : `GET`
**Auth required** : No
**Permissions required** : None
### Success Response
**Code** : `200 OK`
**Content examples**
```json
{
"message": "i am alive"
}
```

26
docs/api/ep_readyness.md Normal file
View file

@ -0,0 +1,26 @@
# Readyness
This endpoint works as heathz-check. See e.g. here:
https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
Other than the liveness-endpoint, this also checks whether specter is functional from a user-point of view (not only up and listening for requests).
**URL** : `/api/healthz/readyness`
**Method** : `GET`
**Auth required** : No
**Permissions required** : None
### Success Response
**Code** : `200 OK`
**Content examples**
```json
{
"message": "i am ready"
}
```

198
docs/api/ep_specter.md Normal file
View file

@ -0,0 +1,198 @@
# Specter Object endpoint
That's a bit of a leaky abstraction. Most of the internals within specter are stored in the specter-object.
This endpoint provides general information from Specter server and Node Status.
**URL** : `/api/v1alpha/specter`
**Method** : `GET`
**Auth required** : YES
**Permissions required** : Admin-User
### Success Response
**Code** : `200 OK`
**Content examples**
```json
{
"data_folder": "/home/some_user/.specter_dev",
"config": {
"auth": {
"method": "usernamepassword",
"password_min_chars": 6,
"rate_limit": "10",
"registration_link_timeout": "1"
},
"explorers": {
"main": "",
"test": "",
"regtest": "",
"signet": ""
},
"explorer_id": {
"main": "CUSTOM",
"test": "CUSTOM",
"regtest": "CUSTOM",
"signet": "CUSTOM"
},
"active_node_alias": "default",
"proxy_url": "socks5h://localhost:9050",
"only_tor": false,
"tor_control_port": "",
"tor_status": false,
"hwi_bridge_url": "/hwi/api/",
"uid": "",
"unit": "btc",
"price_check": false,
"alt_rate": 1,
"alt_symbol": "BTC",
"price_provider": "",
"weight_unit": "oz",
"validate_merkle_proofs": false,
"fee_estimator": "mempool",
"fee_estimator_custom_url": "",
"hide_sensitive_info": false,
"bitcoind": false,
"torrc_password": "_-EEy7RlnCLcUurKd1lCEw"
},
"info": {
"chain": "regtest",
"blocks": 1421,
"headers": 1421,
"bestblockhash": "16ba8e83c41ad4b9e43091543c66482d2e54b68ef9fc407543443247c6722ef4",
"difficulty": 4.656542373906925e-10,
"mediantime": 1623851194,
"verificationprogress": 1,
"initialblockdownload": false,
"chainwork": "0000000000000000000000000000000000000000000000000000000000000b1c",
"size_on_disk": 431663,
"pruned": false,
"softforks": {
"bip34": {
"type": "buried",
"active": true,
"height": 500
},
"bip66": {
"type": "buried",
"active": true,
"height": 1251
},
"bip65": {
"type": "buried",
"active": true,
"height": 1351
},
"csv": {
"type": "buried",
"active": true,
"height": 432
},
"segwit": {
"type": "buried",
"active": true,
"height": 0
},
"testdummy": {
"type": "bip9",
"bip9": {
"status": "active",
"start_time": 0,
"timeout": 9223372036854776000,
"since": 432
},
"height": 432,
"active": true
}
},
"warnings": "",
"mempool_info": {
"loaded": true,
"size": 0,
"bytes": 0,
"usage": 64,
"maxmempool": 300000000,
"mempoolminfee": 1e-05,
"minrelaytxfee": 1e-05
},
"uptime": 13607,
"blockfilterindex": false,
"utxorescan": null
},
"network_info": {
"version": 200100,
"subversion": "/Satoshi:0.20.1/",
"protocolversion": 70015,
"localservices": "0000000000000409",
"localservicesnames": [
"NETWORK",
"WITNESS",
"NETWORK_LIMITED"
],
"localrelay": true,
"timeoffset": 0,
"networkactive": true,
"connections": 0,
"networks": [
{
"name": "ipv4",
"limited": false,
"reachable": true,
"proxy": "",
"proxy_randomize_credentials": false
},
{
"name": "ipv6",
"limited": false,
"reachable": true,
"proxy": "",
"proxy_randomize_credentials": false
},
{
"name": "onion",
"limited": true,
"reachable": false,
"proxy": "",
"proxy_randomize_credentials": false
}
],
"relayfee": 1e-05,
"incrementalfee": 1e-05,
"localaddresses": [
{
"address": "2a02:810d:d00:7700:233e:a7e:ded8:f2da",
"port": 18442,
"score": 1
},
{
"address": "2a02:810d:d00:7700:6534:73c3:85f0:d258",
"port": 18442,
"score": 1
}
],
"warnings": ""
},
"device_manager_datafolder": "/home/some_user/.specter_dev/devices",
"devices_names": [
"MyColdcard"
],
"wallets_names": [
"MyColdcard"
],
"last_update": "06/16/2021, 15:48:04",
"alias_name": {
"mycoldcard": "MyColdcard"
},
"name_alias": {
"MyColdcard": "mycoldcard"
},
"wallets_alias": [
"mycoldcard"
]
}
```

View file

@ -0,0 +1,46 @@
# Full Transaction List
Gives a full tx_list of all transactions. Transactions are cached within specter, so might not be 100% up-to-date.
The result here is highly dependent on the user executing calling this resource as this is not specific to a specific wallet but returns ALL of the TXs of all the wallets.
**URL** : `/api/v1alpha/specter/full_txlist`
**Method** : `GET`
**Auth required** : YES
**Permissions required** : None
### Success Response
**Code** : `200 OK`
**Content examples**
```json
[
{
"txid": "24defbc6161715abcfe56f89eb43bb49d29232b0117affde5483d49e79778f51",
"blockhash": "11792c7d30adf202b9210999b79165f1613f46aa833eeefc5aeab827e02d715e",
"blockheight": 305,
"time": 1624977105,
"conflicts": [],
"bip125-replaceable": "no",
"hex": "02000000000101bc574fc432597ef4f353178a85d3c03179e71a62ee1f5f63648d81097c4935510000000000feffffff020094357700000000160014c757cba1e8ffe37f0495c6e7488e52994df8012efc59cd1d00000000160014529002f66fae537c9320af29b7e0468c7f5bd1870247304402207a3cecbbe45082d85bdbf90d98459fcac376076d92637169ae9633b5e6c25df9022058c4a925a9e66cc11d161039d923593fd78a8cca663009b68fcbbe2de0d84b510121023c9190534406dd37c320d3a168e78e540c5de83fd179ec49e7a3bae13237b8fc10010000",
"vsize": 141,
"category": "receive",
"address": "bcrt1qcatuhg0gll3h7py4cmn53rjjn9xlsqfwj3zcej",
"amount": 20,
"ismine": true,
"confirmations": 272,
"label": "Address #0",
"validated_blockhash": "",
"wallet_alias": "simple_3"
},
{
...
}
]
```

214
docs/api/ep_wallets_psbt.md Normal file
View file

@ -0,0 +1,214 @@
# PSBT Endpoint
**URL** : `/v1alpha/wallets/<wallet_alias>/psbt`
## GET
**Method** : `GET`
**Auth required** : Yes
**Permissions required** : Access to the wallet
### Success Response
**Code** : `200 OK`
**Content examples**
### Get Result
```json
{
"result": {
"a49d234652fc811650bfb3e9a29dcc8a902a2155dbbda8ca8cd1af250f547e41": {
"tx": {
"txid": "a49d234652fc811650bfb3e9a29dcc8a902a2155dbbda8ca8cd1af250f547e41",
"hash": "a49d234652fc811650bfb3e9a29dcc8a902a2155dbbda8ca8cd1af250f547e41",
"version": 2,
"size": 113,
"vsize": 113,
"weight": 452,
"locktime": 0,
"vin": [
{
"txid": "24defbc6161715abcfe56f89eb43bb49d29232b0117affde5483d49e79778f51",
"vout": 0,
"scriptSig": {
"asm": "",
"hex": ""
},
"sequence": 4294967293
}
],
"vout": [
{
"value": 19.98991399,
"n": 0,
"scriptPubKey": {
"asm": "0 5a8ec7c55e3ff7b37fe481cbdc35a150684a89b0",
"hex": "00145a8ec7c55e3ff7b37fe481cbdc35a150684a89b0",
"reqSigs": 1,
"type": "witness_v0_keyhash",
"addresses": [
"bcrt1qt28v03278lmmxllys89acddp2p5y4zds94944n"
]
}
},
{
"value": 0.01,
"n": 1,
"scriptPubKey": {
"asm": "0 c757cba1e8ffe37f0495c6e7488e52994df8012e",
"hex": "0014c757cba1e8ffe37f0495c6e7488e52994df8012e",
"reqSigs": 1,
"type": "witness_v0_keyhash",
"addresses": [
"bcrt1qcatuhg0gll3h7py4cmn53rjjn9xlsqfwj3zcej"
]
}
}
]
},
"unknown": {},
"inputs": [
{
"witness_utxo": {
"amount": 20,
"scriptPubKey": {
"asm": "0 c757cba1e8ffe37f0495c6e7488e52994df8012e",
"hex": "0014c757cba1e8ffe37f0495c6e7488e52994df8012e",
"type": "witness_v0_keyhash",
"address": "bcrt1qcatuhg0gll3h7py4cmn53rjjn9xlsqfwj3zcej"
}
},
"non_witness_utxo": {
"txid": "24defbc6161715abcfe56f89eb43bb49d29232b0117affde5483d49e79778f51",
"hash": "24defbc6161715abcfe56f89eb43bb49d29232b0117affde5483d49e79778f51",
"version": 2,
"size": 113,
"vsize": 113,
"weight": 452,
"locktime": 272,
"vin": [
{
"txid": "5135497c09818d64635f1fee621ae77931c0d3858a1753f3f47e5932c44f57bc",
"vout": 0,
"scriptSig": {
"asm": "",
"hex": ""
},
"sequence": 4294967294
}
],
"vout": [
{
"value": 20,
"n": 0,
"scriptPubKey": {
"asm": "0 c757cba1e8ffe37f0495c6e7488e52994df8012e",
"hex": "0014c757cba1e8ffe37f0495c6e7488e52994df8012e",
"reqSigs": 1,
"type": "witness_v0_keyhash",
"addresses": [
"bcrt1qcatuhg0gll3h7py4cmn53rjjn9xlsqfwj3zcej"
]
}
},
{
"value": 4.9999718,
"n": 1,
"scriptPubKey": {
"asm": "0 529002f66fae537c9320af29b7e0468c7f5bd187",
"hex": "0014529002f66fae537c9320af29b7e0468c7f5bd187",
"reqSigs": 1,
"type": "witness_v0_keyhash",
"addresses": [
"bcrt1q22gq9an04efheyeq4u5m0czx33l4h5v8yfk2m3"
]
}
}
]
},
"bip32_derivs": [
{
"pubkey": "02d02aeb0a1efc029fce0d61c2c5460fd6cac1ca4609bf4aa0d30d0aa462e7dae5",
"master_fingerprint": "1ef4e492",
"path": "m/84'/1'/0'/0/0"
}
]
}
],
"outputs": [
{
"bip32_derivs": [
{
"pubkey": "0270537e123805be08ed48ddcc5961c01607643f42cc11fdcf18f709c40588984a",
"master_fingerprint": "1ef4e492",
"path": "m/84'/1'/0'/1/0"
}
]
},
{
"bip32_derivs": [
{
"pubkey": "02d02aeb0a1efc029fce0d61c2c5460fd6cac1ca4609bf4aa0d30d0aa462e7dae5",
"master_fingerprint": "1ef4e492",
"path": "m/84'/1'/0'/0/0"
}
]
}
],
"fee": 8.601e-05,
"fee_rate": "0.00061000",
"tx_full_size": 141,
"base64": "cHNidP8BAHECAAAAAVGPd3me1INU3v96EbAyktJJu0PriW/lz6sVFxbG+94kAAAAAAD9////AicwJncAAAAAFgAUWo7HxV4/97N/5IHL3DWhUGhKibBAQg8AAAAAABYAFMdXy6Ho/+N/BJXG50iOUplN+AEuAAAAAAABAHECAAAAAbxXT8QyWX7081MXioXTwDF55xpi7h9fY2SNgQl8STVRAAAAAAD+////AgCUNXcAAAAAFgAUx1fLoej/438ElcbnSI5SmU34AS78Wc0dAAAAABYAFFKQAvZvrlN8kyCvKbfgRox/W9GHEAEAAAEBHwCUNXcAAAAAFgAUx1fLoej/438ElcbnSI5SmU34AS4iBgLQKusKHvwCn84NYcLFRg/WysHKRgm/SqDTDQqkYufa5Rge9OSSVAAAgAEAAIAAAACAAAAAAAAAAAAAIgICcFN+EjgFvgjtSN3MWWHAFgdkP0LMEf3PGPcJxAWImEoYHvTkklQAAIABAACAAAAAgAEAAAAAAAAAACICAtAq6woe/AKfzg1hwsVGD9bKwcpGCb9KoNMNCqRi59rlGB705JJUAACAAQAAgAAAAIAAAAAAAAAAAAA=",
"amount": [
0.01
],
"address": [
"bcrt1qcatuhg0gll3h7py4cmn53rjjn9xlsqfwj3zcej"
],
"time": 1624978624.173007,
"sigs_count": 0
}
}
}
```
## POST
**Method** : `POST`
**Auth required** : YES
**Permissions required** : Access to the wallet
```
curl -u admin:password -X POST http://127.0.0.1:25441/api/v1alpha/wallets/simple_3/psbt \
-H 'Content-Type: application/json' \
-d \
'
{
"recipients" : [
{
"address": "BCRT1qgc6h85z43g3ss2dl5zdrzrp3ef6av4neqcqhh8",
"amount": 0.1,
"unit": "btc",
"label": "someLabel"
},
{
"address": "bcrt1q3kfetuxpxvujasww6xas94nawklvpz0e52uw8a",
"amount": 111211,
"unit": "sat",
"label": "someOtherLabel"
}
],
"rbf_tx_id": "",
"subtract_from": "1",
"fee_rate": "64",
"rbf": true
}'
```
As a result, you get the created PSBT as in the GET-Request.

View file

@ -0,0 +1,167 @@
## Wallet
This API will return wallet balance details as well as transactions.
**URL** : `/api/v1alpha/wallets/<wallet_alias>`
**Method** : `GET`
**Auth required** : Yes
**Permissions required** : Access to the wallet
### Success Response
**Code** : `200 OK`
**Content examples**
```
{
"simple_3": {
"name": "Simple",
"alias": "simple_3",
"description": "Single (Segwit)",
"address_type": "bech32",
"address": "bcrt1qsqnuk9hulcfta7kj7687favjv66d5e9yy0lr7t",
"address_index": 1,
"change_address": "bcrt1qt28v03278lmmxllys89acddp2p5y4zds94944n",
"change_index": 0,
"keypool": 300,
"change_keypool": 300,
"recv_descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/0/*)#xp8lv5nr",
"change_descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/1/*)#h4z73prm",
"keys": [
"Key"
],
"devices": [
"Trezor"
],
"sigs_required": 1,
"pending_psbts": {},
"frozen_utxo": [],
"fullpath": "/home/kim/.specter_dev/wallets/regtest/simple_3.json",
"manager": "WalletManager",
"rpc": "BitcoinRPC",
"last_block": "338e9672c7f71140a3cb0c42fa9f064083b1b13a379242ba1180cff2355478a5",
"_addresses": {
"bcrt1qcatuhg0gll3h7py4cmn53rjjn9xlsqfwj3zcej": {
"address": "bcrt1qcatuhg0gll3h7py4cmn53rjjn9xlsqfwj3zcej",
"index": 0,
"change": false,
"label": null,
"used": true
},
"bcrt1qsqnuk9hulcfta7kj7687favjv66d5e9yy0lr7t": {
"address": "bcrt1qsqnuk9hulcfta7kj7687favjv66d5e9yy0lr7t",
"index": 1,
"change": false,
"label": null,
"used": null
},
...
},
"_transactions": {
"24defbc6161715abcfe56f89eb43bb49d29232b0117affde5483d49e79778f51": {
"txid": "24defbc6161715abcfe56f89eb43bb49d29232b0117affde5483d49e79778f51",
"blockheight": 305,
"blockhash": "11792c7d30adf202b9210999b79165f1613f46aa833eeefc5aeab827e02d715e",
"time": 1624977105,
"conflicts": [],
"bip125-replaceable": "no",
"hex": "02000000000101bc574fc432597ef4f353178a85d3c03179e71a62ee1f5f63648d81097c4935510000000000feffffff020094357700000000160014c757cba1e8ffe37f0495c6e7488e52994df8012efc59cd1d00000000160014529002f66fae537c9320af29b7e0468c7f5bd1870247304402207a3cecbbe45082d85bdbf90d98459fcac376076d92637169ae9633b5e6c25df9022058c4a925a9e66cc11d161039d923593fd78a8cca663009b68fcbbe2de0d84b510121023c9190534406dd37c320d3a168e78e540c5de83fd179ec49e7a3bae13237b8fc10010000",
"vsize": 141,
"category": "receive",
"address": "bcrt1qcatuhg0gll3h7py4cmn53rjjn9xlsqfwj3zcej",
"amount": 20,
"ismine": true
}
},
"info": {
"walletname": "specter/simple_3",
"walletversion": 169900,
"balance": 0,
"unconfirmed_balance": 0,
"immature_balance": 0,
"txcount": 1,
"keypoololdest": 1624977103,
"keypoolsize": 300,
"keypoolsize_hd_internal": 301,
"paytxfee": 0,
"private_keys_enabled": false,
"avoid_reuse": false,
"scanning": false
},
"full_utxo": [
{
"txid": "24defbc6161715abcfe56f89eb43bb49d29232b0117affde5483d49e79778f51",
"vout": 0,
"address": "bcrt1qcatuhg0gll3h7py4cmn53rjjn9xlsqfwj3zcej",
"label": "",
"scriptPubKey": "0014c757cba1e8ffe37f0495c6e7488e52994df8012e",
"amount": 20,
"confirmations": 286,
"spendable": false,
"solvable": true,
"desc": "wpkh([1ef4e492/84'/1'/0'/0/0]02d02aeb0a1efc029fce0d61c2c5460fd6cac1ca4609bf4aa0d30d0aa462e7dae5)#3m9y9l9z",
"safe": true,
"time": 1624977105,
"category": "receive",
"locked": false
}
],
"balance": {
"trusted": 20,
"untrusted_pending": 0,
"immature": 0,
"available": {
"trusted": 20,
"untrusted_pending": 0,
"immature": 0
}
}
},
"txlist": [
{
"txid": "24defbc6161715abcfe56f89eb43bb49d29232b0117affde5483d49e79778f51",
"blockhash": "11792c7d30adf202b9210999b79165f1613f46aa833eeefc5aeab827e02d715e",
"blockheight": 305,
"time": 1624977105,
"conflicts": [],
"bip125-replaceable": "no",
"hex": "02000000000101bc574fc432597ef4f353178a85d3c03179e71a62ee1f5f63648d81097c4935510000000000feffffff020094357700000000160014c757cba1e8ffe37f0495c6e7488e52994df8012efc59cd1d00000000160014529002f66fae537c9320af29b7e0468c7f5bd1870247304402207a3cecbbe45082d85bdbf90d98459fcac376076d92637169ae9633b5e6c25df9022058c4a925a9e66cc11d161039d923593fd78a8cca663009b68fcbbe2de0d84b510121023c9190534406dd37c320d3a168e78e540c5de83fd179ec49e7a3bae13237b8fc10010000",
"vsize": 141,
"category": "receive",
"address": "bcrt1qcatuhg0gll3h7py4cmn53rjjn9xlsqfwj3zcej",
"amount": 20,
"ismine": true,
"confirmations": 286,
"label": "Address #0",
"validated_blockhash": "",
"wallet_alias": "simple_3"
}
],
"scan": null,
"address_index": 1,
"utxo": [
{
"txid": "24defbc6161715abcfe56f89eb43bb49d29232b0117affde5483d49e79778f51",
"vout": 0,
"address": "bcrt1qcatuhg0gll3h7py4cmn53rjjn9xlsqfwj3zcej",
"label": "",
"scriptPubKey": "0014c757cba1e8ffe37f0495c6e7488e52994df8012e",
"amount": 20,
"confirmations": 286,
"spendable": false,
"solvable": true,
"desc": "wpkh([1ef4e492/84'/1'/0'/0/0]02d02aeb0a1efc029fce0d61c2c5460fd6cac1ca4609bf4aa0d30d0aa462e7dae5)#3m9y9l9z",
"safe": true,
"time": 1624977105,
"category": "receive",
"locked": false
}
]
}
```

View file

@ -6,6 +6,8 @@ Flask==1.1.2
Flask-Babel==2.0.0
Flask-Cors==3.0.9
Flask-Login==0.5.0
Flask-RESTful==0.3.8
Flask-HTTPAuth==4.2.0
hwi==2.0.1
importlib_metadata==2.0.0
pyserial==3.4

View file

@ -1,136 +1,143 @@
#
# This file is autogenerated by pip-compile with python 3.8
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --generate-hashes requirements.in
#
aniso8601==9.0.1 \
--hash=sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f \
--hash=sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973 \
# via flask-restful
babel==2.9.1 \
--hash=sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9 \
--hash=sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0
--hash=sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0 \
# via flask-babel
base58==2.0.1 \
--hash=sha256:365c9561d9babac1b5f18ee797508cd54937a724b6e419a130abad69cec5ca79 \
--hash=sha256:447adc750d6b642987ffc6d397ecd15a799852d5f6a1d308d384500243825058
base58==2.1.0 \
--hash=sha256:171a547b4a3c61e1ae3807224a6f7aec75e364c4395e7562649d7335768001a2 \
--hash=sha256:8225891d501b68c843ffe30b86371f844a21c6ba00da76f52f9b998ba771fb48 \
# via bitbox02
bitbox02==5.3.0 \
--hash=sha256:797e6904d431f6d2ef711f169e7ce8fffc125cc8c5b3efb8187fd451f45635e1 \
--hash=sha256:fe0e8aeb9b32fd7d76bb3e9838895973a74dfd532a8fb8ac174a1a60214aee26
--hash=sha256:fe0e8aeb9b32fd7d76bb3e9838895973a74dfd532a8fb8ac174a1a60214aee26 \
# via hwi
cbor==1.0.0 \
--hash=sha256:13225a262ddf5615cbd9fd55a76a0d53069d18b07d2e9f19c39e6acb8609bbb6
--hash=sha256:13225a262ddf5615cbd9fd55a76a0d53069d18b07d2e9f19c39e6acb8609bbb6 \
# via -r requirements.in
certifi==2019.9.11 \
--hash=sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50 \
--hash=sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef
# via
# -r requirements.in
# requests
cffi==1.14.3 \
--hash=sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d \
--hash=sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b \
--hash=sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4 \
--hash=sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f \
--hash=sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3 \
--hash=sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579 \
--hash=sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537 \
--hash=sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e \
--hash=sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05 \
--hash=sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171 \
--hash=sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca \
--hash=sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522 \
--hash=sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c \
--hash=sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc \
--hash=sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d \
--hash=sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808 \
--hash=sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828 \
--hash=sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869 \
--hash=sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d \
--hash=sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9 \
--hash=sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0 \
--hash=sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc \
--hash=sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15 \
--hash=sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c \
--hash=sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a \
--hash=sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3 \
--hash=sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1 \
--hash=sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768 \
--hash=sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d \
--hash=sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b \
--hash=sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e \
--hash=sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d \
--hash=sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730 \
--hash=sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394 \
--hash=sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1 \
--hash=sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591
--hash=sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef \
# via -r requirements.in, requests
cffi==1.14.5 \
--hash=sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813 \
--hash=sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373 \
--hash=sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69 \
--hash=sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f \
--hash=sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06 \
--hash=sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05 \
--hash=sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea \
--hash=sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee \
--hash=sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0 \
--hash=sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396 \
--hash=sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7 \
--hash=sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f \
--hash=sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73 \
--hash=sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315 \
--hash=sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76 \
--hash=sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1 \
--hash=sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49 \
--hash=sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed \
--hash=sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892 \
--hash=sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482 \
--hash=sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058 \
--hash=sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5 \
--hash=sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53 \
--hash=sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045 \
--hash=sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3 \
--hash=sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55 \
--hash=sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5 \
--hash=sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e \
--hash=sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c \
--hash=sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369 \
--hash=sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827 \
--hash=sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053 \
--hash=sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa \
--hash=sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4 \
--hash=sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322 \
--hash=sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132 \
--hash=sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62 \
--hash=sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa \
--hash=sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0 \
--hash=sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396 \
--hash=sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e \
--hash=sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991 \
--hash=sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6 \
--hash=sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc \
--hash=sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1 \
--hash=sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406 \
--hash=sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333 \
--hash=sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d \
--hash=sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c \
# via cryptography
chardet==3.0.4 \
--hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691
# via
# -r requirements.in
# requests
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 \
# via -r requirements.in, requests
click==7.1.2 \
--hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a \
--hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc
# via
# -r requirements.in
# flask
cryptography==3.3.2 \
--hash=sha256:0d7b69674b738068fa6ffade5c962ecd14969690585aaca0a1b1fc9058938a72 \
--hash=sha256:1bd0ccb0a1ed775cd7e2144fe46df9dc03eefd722bbcf587b3e0616ea4a81eff \
--hash=sha256:3c284fc1e504e88e51c428db9c9274f2da9f73fdf5d7e13a36b8ecb039af6e6c \
--hash=sha256:49570438e60f19243e7e0d504527dd5fe9b4b967b5a1ff21cc12b57602dd85d3 \
--hash=sha256:541dd758ad49b45920dda3b5b48c968f8b2533d8981bcdb43002798d8f7a89ed \
--hash=sha256:5a60d3780149e13b7a6ff7ad6526b38846354d11a15e21068e57073e29e19bed \
--hash=sha256:7951a966613c4211b6612b0352f5bf29989955ee592c4a885d8c7d0f830d0433 \
--hash=sha256:922f9602d67c15ade470c11d616f2b2364950602e370c76f0c94c94ae672742e \
--hash=sha256:a0f0b96c572fc9f25c3f4ddbf4688b9b38c69836713fb255f4a2715d93cbaf44 \
--hash=sha256:a777c096a49d80f9d2979695b835b0f9c9edab73b59e4ceb51f19724dda887ed \
--hash=sha256:a9a4ac9648d39ce71c2f63fe7dc6db144b9fa567ddfc48b9fde1b54483d26042 \
--hash=sha256:aa4969f24d536ae2268c902b2c3d62ab464b5a66bcb247630d208a79a8098e9b \
--hash=sha256:c7390f9b2119b2b43160abb34f63277a638504ef8df99f11cb52c1fda66a2e6f \
--hash=sha256:e18e6ab84dfb0ab997faf8cca25a86ff15dfea4027b986322026cc99e0a892da
# via
# noiseprotocol
# pgpy
# pyopenssl
--hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc \
# via -r requirements.in, flask
cryptography==3.4.7 \
--hash=sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d \
--hash=sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959 \
--hash=sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6 \
--hash=sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873 \
--hash=sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2 \
--hash=sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713 \
--hash=sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1 \
--hash=sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177 \
--hash=sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250 \
--hash=sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca \
--hash=sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d \
--hash=sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9 \
# via noiseprotocol, pgpy, pyopenssl
daemonize==2.5.0 \
--hash=sha256:9b6b91311a9d934ff3f5f766666635ca280d3de8e7137e4cd7d3f052543b989f \
--hash=sha256:dd026e4ff8d22cb016ed2130bc738b7d4b1da597ef93c074d2adb9e4dea08bc3
--hash=sha256:dd026e4ff8d22cb016ed2130bc738b7d4b1da597ef93c074d2adb9e4dea08bc3 \
# via -r requirements.in
ecdsa==0.16.1 \
--hash=sha256:881fa5e12bb992972d3d1b3d4dfbe149ab76a89f13da02daa5ea1ec7dea6e747 \
--hash=sha256:cfc046a2ddd425adbd1a78b3c46f0d1325c657811c0f45ecc3a0a6236c1e50ff
# via
# bitbox02
# hwi
ecdsa==0.17.0 \
--hash=sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676 \
--hash=sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa \
# via bitbox02, hwi
embit==0.4.2 \
--hash=sha256:d67fc0f7fbdb7588c3eb24441bf8e05770056260bc8e5537399a1b3ce5ccf12a
--hash=sha256:d67fc0f7fbdb7588c3eb24441bf8e05770056260bc8e5537399a1b3ce5ccf12a \
# via -r requirements.in
flask==1.1.2 \
--hash=sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060 \
--hash=sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557
# via
# -r requirements.in
# flask-babel
# flask-cors
# flask-login
# flask-wtf
flask-babel==2.0.0 \
--hash=sha256:e6820a052a8d344e178cdd36dd4bb8aea09b4bda3d5f9fa9f008df2c7f2f5468 \
--hash=sha256:f9faf45cdb2e1a32ea2ec14403587d4295108f35017a7821a2b1acb8cfd9257d
--hash=sha256:f9faf45cdb2e1a32ea2ec14403587d4295108f35017a7821a2b1acb8cfd9257d \
# via -r requirements.in
flask-cors==3.0.9 \
--hash=sha256:6bcfc100288c5d1bcb1dbb854babd59beee622ffd321e444b05f24d6d58466b8 \
--hash=sha256:cee4480aaee421ed029eaa788f4049e3e26d15b5affb6a880dade6bafad38324
--hash=sha256:cee4480aaee421ed029eaa788f4049e3e26d15b5affb6a880dade6bafad38324 \
# via -r requirements.in
flask-httpauth==4.2.0 \
--hash=sha256:3fcedb99a03985915335a38c35bfee6765cbd66d7f46440fa3b42ae94a90fac7 \
--hash=sha256:8c7e49e53ce7dc14e66fe39b9334e4b7ceb8d0b99a6ba1c3562bb528ef9da84a \
# via -r requirements.in
flask-login==0.5.0 \
--hash=sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b \
--hash=sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0
--hash=sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0 \
# via -r requirements.in
flask-restful==0.3.8 \
--hash=sha256:5ea9a5991abf2cb69b4aac19793faac6c032300505b325687d7c305ffaa76915 \
--hash=sha256:d891118b951921f1cec80cabb4db98ea6058a35e6404788f9e70d5b243813ec2 \
# via -r requirements.in
flask==1.1.2 \
--hash=sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060 \
--hash=sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557 \
# via -r requirements.in, flask-babel, flask-cors, flask-httpauth, flask-login, flask-restful, flask-wtf
flask_wtf==0.14.3 \
--hash=sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2 \
--hash=sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720
--hash=sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720 \
# via -r requirements.in
hidapi==0.10.1 \
--hash=sha256:095798ae1b3d6892fb0eb7ba1ab06054f6fafe6d09bc3714d80fdbf227c98f87 \
@ -153,106 +160,109 @@ hidapi==0.10.1 \
--hash=sha256:b1becc9f09c85c473e91cf869b592d5d87fb8b89672988de33776b20b4c53ce1 \
--hash=sha256:b686b2b547890c8ed17ebeabded0050ce377180a56daefa20822b4d66d3a5dea \
--hash=sha256:df4a23cd03f00d5cdc603252650df82cdd1923ceef6811cb029cc9d11a9a7a61 \
--hash=sha256:f49a0de45217366b85597c2edb4be8bd61c9f26f533b854b058dded4352dd89d
# via
# bitbox02
# hwi
--hash=sha256:f49a0de45217366b85597c2edb4be8bd61c9f26f533b854b058dded4352dd89d \
# via bitbox02, hwi
hwi==2.0.1 \
--hash=sha256:1a49ec86d4770239408e74c87acdd15b9f72271fbdddf66acd935f068bbf115d \
--hash=sha256:cba5e254fcf7ae1b523ec1f0eab693b401d4b3cd949b35a4557a106a98065c23
--hash=sha256:cba5e254fcf7ae1b523ec1f0eab693b401d4b3cd949b35a4557a106a98065c23 \
# via -r requirements.in
idna==2.10 \
--hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \
--hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0
--hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 \
# via requests
importlib_metadata==2.0.0 \
--hash=sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da \
--hash=sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3
--hash=sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3 \
# via -r requirements.in
itsdangerous==1.1.0 \
--hash=sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19 \
--hash=sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749
# via
# flask
# flask-wtf
jinja2==2.11.3 \
--hash=sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419 \
--hash=sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6
# via
# flask
# flask-babel
libusb1==1.8 \
--hash=sha256:240f65ac70ba3fab77749ec84a412e4e89624804cb80d6c9d394eef5af8878d6
itsdangerous==2.0.1 \
--hash=sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c \
--hash=sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0 \
# via flask, flask-wtf
jinja2==3.0.1 \
--hash=sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4 \
--hash=sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4 \
# via flask, flask-babel
libusb1==1.9.2 \
--hash=sha256:27aec6aa1ff9ca845d0035023f3cf39710afac56903c51cd96a95404d064189e \
--hash=sha256:2dff68819350bf8a8c157c7fa40d3efc741cb57868687d1714c8125ee99e8ac8 \
--hash=sha256:8ee4a963d4ecc20d9f4543b9151729c9cc9a229c2f9119e12bff762e84d8859f \
--hash=sha256:a323588902fbd3693f8fddd7eac016700b24116c31b00756b9f52cf06c2a6629 \
--hash=sha256:b4f25a2d66f62ec740edba3597038a7e9cd45b43456acfdb7a2bca8c2ad4aa30 \
--hash=sha256:c19d49136ef262474dbbac8bd40a2c4b65660220571de8564efec631c56bdc09 \
--hash=sha256:c3dd4df43b5c38f65bf599413810d021f5f98396c4b6f66765fb98193aca11b0 \
# via hwi
markupsafe==1.1.1 \
--hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \
--hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \
--hash=sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235 \
--hash=sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5 \
--hash=sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42 \
--hash=sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff \
--hash=sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b \
--hash=sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1 \
--hash=sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e \
--hash=sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183 \
--hash=sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66 \
--hash=sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b \
--hash=sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1 \
--hash=sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15 \
--hash=sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1 \
--hash=sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e \
--hash=sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b \
--hash=sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905 \
--hash=sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735 \
--hash=sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d \
--hash=sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e \
--hash=sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d \
--hash=sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c \
--hash=sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21 \
--hash=sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2 \
--hash=sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5 \
--hash=sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b \
--hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \
--hash=sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f \
--hash=sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f \
--hash=sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2 \
--hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 \
--hash=sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be
# via
# jinja2
# wtforms
markupsafe==2.0.1 \
--hash=sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298 \
--hash=sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64 \
--hash=sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b \
--hash=sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567 \
--hash=sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff \
--hash=sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74 \
--hash=sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35 \
--hash=sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26 \
--hash=sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7 \
--hash=sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75 \
--hash=sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f \
--hash=sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135 \
--hash=sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8 \
--hash=sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a \
--hash=sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914 \
--hash=sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18 \
--hash=sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8 \
--hash=sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2 \
--hash=sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d \
--hash=sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b \
--hash=sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f \
--hash=sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb \
--hash=sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833 \
--hash=sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415 \
--hash=sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902 \
--hash=sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9 \
--hash=sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d \
--hash=sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066 \
--hash=sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f \
--hash=sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5 \
--hash=sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94 \
--hash=sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509 \
--hash=sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51 \
--hash=sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872 \
# via jinja2, wtforms
mnemonic==0.19 \
--hash=sha256:4e37eb02b2cbd56a0079cabe58a6da93e60e3e4d6e757a586d9f23d96abea931 \
--hash=sha256:a8d78c5100acfa7df9bab6b9db7390831b0e54490934b718ff9efd68f0d731a6
# via
# -r requirements.in
# hwi
--hash=sha256:a8d78c5100acfa7df9bab6b9db7390831b0e54490934b718ff9efd68f0d731a6 \
# via -r requirements.in, hwi
noiseprotocol==0.3.1 \
--hash=sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111
--hash=sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111 \
--hash=sha256:b092a871b60f6a8f07f17950dc9f7098c8fe7d715b049bd4c24ee3752b90d645 \
# via bitbox02
pgpy==0.5.3 \
--hash=sha256:a49c269cedcaf82ac6999bcae5fd3f543ecb1c759f9d48a15ad8d8fa4ac03987 \
--hash=sha256:cba6fbbb44a896a8a4f5807b3d8d4943a8f7a6607be11587f4a27734c711c1dd
--hash=sha256:cba6fbbb44a896a8a4f5807b3d8d4943a8f7a6607be11587f4a27734c711c1dd \
# via -r requirements.in
protobuf==3.13.0 \
--hash=sha256:0bba42f439bf45c0f600c3c5993666fcb88e8441d011fad80a11df6f324eef33 \
--hash=sha256:1e834076dfef9e585815757a2c7e4560c7ccc5962b9d09f831214c693a91b463 \
--hash=sha256:339c3a003e3c797bc84499fa32e0aac83c768e67b3de4a5d7a5a9aa3b0da634c \
--hash=sha256:361acd76f0ad38c6e38f14d08775514fbd241316cce08deb2ce914c7dfa1184a \
--hash=sha256:3dee442884a18c16d023e52e32dd34a8930a889e511af493f6dc7d4d9bf12e4f \
--hash=sha256:4d1174c9ed303070ad59553f435846a2f877598f59f9afc1b89757bdf846f2a7 \
--hash=sha256:5db9d3e12b6ede5e601b8d8684a7f9d90581882925c96acf8495957b4f1b204b \
--hash=sha256:6a82e0c8bb2bf58f606040cc5814e07715b2094caeba281e2e7d0b0e2e397db5 \
--hash=sha256:8c35bcbed1c0d29b127c886790e9d37e845ffc2725cc1db4bd06d70f4e8359f4 \
--hash=sha256:91c2d897da84c62816e2f473ece60ebfeab024a16c1751aaf31100127ccd93ec \
--hash=sha256:9c2e63c1743cba12737169c447374fab3dfeb18111a460a8c1a000e35836b18c \
--hash=sha256:9edfdc679a3669988ec55a989ff62449f670dfa7018df6ad7f04e8dbacb10630 \
--hash=sha256:c0c5ab9c4b1eac0a9b838f1e46038c3175a95b0f2d944385884af72876bd6bc7 \
--hash=sha256:c8abd7605185836f6f11f97b21200f8a864f9cb078a193fe3c9e235711d3ff1e \
--hash=sha256:d69697acac76d9f250ab745b46c725edf3e98ac24763990b24d58c16c642947a \
--hash=sha256:df3932e1834a64b46ebc262e951cd82c3cf0fa936a154f0a42231140d8237060 \
--hash=sha256:e7662437ca1e0c51b93cadb988f9b353fa6b8013c0385d63a70c8a77d84da5f9 \
--hash=sha256:f68eb9d03c7d84bd01c790948320b768de8559761897763731294e3bc316decb
protobuf==3.17.3 \
--hash=sha256:13ee7be3c2d9a5d2b42a1030976f760f28755fcf5863c55b1460fd205e6cd637 \
--hash=sha256:145ce0af55c4259ca74993ddab3479c78af064002ec8227beb3d944405123c71 \
--hash=sha256:14c1c9377a7ffbeaccd4722ab0aa900091f52b516ad89c4b0c3bb0a4af903ba5 \
--hash=sha256:1556a1049ccec58c7855a78d27e5c6e70e95103b32de9142bae0576e9200a1b0 \
--hash=sha256:26010f693b675ff5a1d0e1bdb17689b8b716a18709113288fead438703d45539 \
--hash=sha256:2ae692bb6d1992afb6b74348e7bb648a75bb0d3565a3f5eea5bec8f62bd06d87 \
--hash=sha256:2bfb815216a9cd9faec52b16fd2bfa68437a44b67c56bee59bc3926522ecb04e \
--hash=sha256:4ffbd23640bb7403574f7aff8368e2aeb2ec9a5c6306580be48ac59a6bac8bde \
--hash=sha256:6902a1e4b7a319ec611a7345ff81b6b004b36b0d2196ce7a748b3493da3d226d \
--hash=sha256:6ce4d8bf0321e7b2d4395e253f8002a1a5ffbcfd7bcc0a6ba46712c07d47d0b4 \
--hash=sha256:6d847c59963c03fd7a0cd7c488cadfa10cda4fff34d8bc8cba92935a91b7a037 \
--hash=sha256:72804ea5eaa9c22a090d2803813e280fb273b62d5ae497aaf3553d141c4fdd7b \
--hash=sha256:7a4c97961e9e5b03a56f9a6c82742ed55375c4a25f2692b625d4087d02ed31b9 \
--hash=sha256:8727ee027157516e2c311f218ebf2260a18088ffb2d29473e82add217d196b1c \
--hash=sha256:99938f2a2d7ca6563c0ade0c5ca8982264c484fdecf418bd68e880a7ab5730b1 \
--hash=sha256:9b7a5c1022e0fa0dbde7fd03682d07d14624ad870ae52054849d8960f04bc764 \
--hash=sha256:a22b3a0dbac6544dacbafd4c5f6a29e389a50e3b193e2c70dae6bbf7930f651d \
--hash=sha256:a981222367fb4210a10a929ad5983ae93bd5a050a0824fc35d6371c07b78caf6 \
--hash=sha256:ab6bb0e270c6c58e7ff4345b3a803cc59dbee19ddf77a4719c5b635f1d547aa8 \
--hash=sha256:c56c050a947186ba51de4f94ab441d7f04fcd44c56df6e922369cc2e1a92d683 \
--hash=sha256:e76d9686e088fece2450dbc7ee905f9be904e427341d289acbe9ad00b78ebd47 \
--hash=sha256:f0e59430ee953184a703a324b8ec52f571c6c4259d496a19d1cabcdc19dabc62 \
--hash=sha256:ffea251f5cd3c0b9b43c7a7a912777e0bc86263436a87c2555242a348817221b \
# via bitbox02
psutil==5.7.3 \
--hash=sha256:01bc82813fbc3ea304914581954979e637bcc7084e59ac904d870d6eb8bb2bc7 \
@ -265,86 +275,75 @@ psutil==5.7.3 \
--hash=sha256:af73f7bcebdc538eda9cc81d19db1db7bf26f103f91081d780bbacfcb620dee2 \
--hash=sha256:e02c31b2990dcd2431f4524b93491941df39f99619b0d312dfe1d4d530b08b4b \
--hash=sha256:fa38ac15dbf161ab1e941ff4ce39abd64b53fec5ddf60c23290daed2bc7d1157 \
--hash=sha256:fbcac492cb082fa38d88587d75feb90785d05d7e12d4565cbf1ecc727aff71b7
--hash=sha256:fbcac492cb082fa38d88587d75feb90785d05d7e12d4565cbf1ecc727aff71b7 \
# via -r requirements.in
pyaes==1.6.1 \
--hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
--hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f \
# via hwi
pyasn1==0.4.8 \
--hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \
--hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba
--hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba \
# via pgpy
pycparser==2.20 \
--hash=sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0 \
--hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705
--hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705 \
# via cffi
pyopenssl==20.0.1 \
--hash=sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51 \
--hash=sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b
--hash=sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b \
# via -r requirements.in
pyserial==3.4 \
--hash=sha256:6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627 \
--hash=sha256:e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8
--hash=sha256:e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8 \
# via -r requirements.in
pysocks==1.7.1 \
--hash=sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299 \
--hash=sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5 \
--hash=sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0
--hash=sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0 \
# via -r requirements.in
python-dotenv==0.13.0 \
--hash=sha256:25c0ff1a3e12f4bde8d592cc254ab075cfe734fc5dd989036716fd17ee7e5ec7 \
--hash=sha256:3b9909bc96b0edc6b01586e1eed05e71174ef4e04c71da5786370cebea53ad74
--hash=sha256:3b9909bc96b0edc6b01586e1eed05e71174ef4e04c71da5786370cebea53ad74 \
# via -r requirements.in
pytz==2021.1 \
--hash=sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da \
--hash=sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798
# via
# babel
# flask-babel
--hash=sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798 \
# via babel, flask-babel, flask-restful
requests==2.25.0 \
--hash=sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8 \
--hash=sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998
--hash=sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998 \
# via -r requirements.in
semver==2.10.2 \
--hash=sha256:21e80ca738975ed513cba859db0a0d2faca2380aef1962f48272ebf9a8a44bd4 \
--hash=sha256:c0a4a9d1e45557297a722ee9bac3de2ec2ea79016b6ffcaca609b0bc62cf4276
semver==2.13.0 \
--hash=sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4 \
--hash=sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f \
# via bitbox02
six==1.12.0 \
--hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \
--hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73
# via
# -r requirements.in
# cryptography
# ecdsa
# flask-cors
# pgpy
# protobuf
# pyopenssl
--hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73 \
# via -r requirements.in, ecdsa, flask-cors, flask-restful, pgpy, protobuf, pyopenssl
stem==1.8.0 \
--hash=sha256:a0b48ea6224e95f22aa34c0bc3415f0eb4667ddeae3dfb5e32a6920c185568c2
--hash=sha256:a0b48ea6224e95f22aa34c0bc3415f0eb4667ddeae3dfb5e32a6920c185568c2 \
# via -r requirements.in
typing-extensions==3.7.4.3 \
--hash=sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918 \
--hash=sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c \
--hash=sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f
# via
# bitbox02
# hwi
typing-extensions==3.10.0.0 \
--hash=sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497 \
--hash=sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342 \
--hash=sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84 \
# via bitbox02, hwi
urllib3==1.26.5 \
--hash=sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c \
--hash=sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098
--hash=sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098 \
# via requests
werkzeug==1.0.1 \
--hash=sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43 \
--hash=sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c
werkzeug==2.0.1 \
--hash=sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42 \
--hash=sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8 \
# via flask
wtforms==2.3.3 \
--hash=sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c \
--hash=sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c
--hash=sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c \
# via flask-wtf
zipp==3.3.0 \
--hash=sha256:64ad89efee774d1897a58607895d80789c59778ea02185dd846ac38394a8642b \
--hash=sha256:eed8ec0b8d1416b2ca33516a37a08892442f3954dee131e92cfd92d8fe3e7066
zipp==3.4.1 \
--hash=sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76 \
--hash=sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098 \
# via importlib-metadata
# WARNING: The following packages were not pinned, but pip requires them to be

View file

@ -0,0 +1,16 @@
""" API Blueprint Application """
import os
from flask import Flask, Blueprint, session
from flask_restful import Api
from flask_httpauth import HTTPBasicAuth
from flask import current_app as app
api_bp = Blueprint("api_bp", __name__, template_folder="templates", url_prefix="/api")
api_rest = Api(api_bp, decorators=[app.csrf.exempt])
auth = HTTPBasicAuth()
from . import views
from .rest import api

View file

@ -0,0 +1,82 @@
import logging
import json
import cryptoadvance.specter as specter
from ...wallet import Wallet
from .base import (
SecureResource,
rest_resource,
)
from ..security import require_admin, verify_password
from flask_restful import abort
from flask import current_app as app
from datetime import datetime
from ...specter_error import SpecterError
from ...util.fee_estimation import get_fees
from .. import auth
from .resource_healthz import ResourceLiveness, ResourceReadyness
from .resource_psbt import ResourcePsbt
from .resource_specter import ResourceSpecter
from .resource_txlist import ResourceTXlist
logger = logging.getLogger(__name__)
@rest_resource
class ResourceWallet(SecureResource):
endpoints = ["/v1alpha/wallets/<wallet_alias>/"]
def get(self, wallet_alias):
user = auth.current_user()
try:
wallet: Wallet = app.specter.user_manager.get_user(
user
).wallet_manager.get_by_alias(wallet_alias)
except SpecterError as se:
logger.error(se)
if str(se).startswith(f"Wallet {wallet_alias} does not exist!"):
return abort(403, message=f"Wallet {wallet_alias} does not exist")
logger.error(se)
return abort(500)
wallet.get_balance()
wallet.check_utxo()
wallet.check_unused()
return_dict = {}
address_index = wallet.address_index
validate_merkle_proofs = app.specter.config.get("validate_merkle_proofs")
tx_list = []
idx = 0
tx_len = 1
transactions = app.specter.wallet_manager.full_txlist(
fetch_transactions=False, validate_merkle_proofs=validate_merkle_proofs
)
tx_list.append(transactions)
# Flatten the list
flat_list = []
for element in tx_list:
for dic_item in element:
flat_list.append(dic_item)
# Check if scanning
scan = wallet.rescan_progress
return_dict[wallet_alias] = wallet.__dict__
return_dict["txlist"] = flat_list
return_dict["scan"] = scan
return_dict["address_index"] = address_index
return_dict["utxo"] = wallet.utxo
# Serialize only objects that are json compatible
# This will exclude classes and methods
def safe_serialize(obj):
def default(o):
return f"{type(o).__qualname__}"
return json.dumps(obj, default=default)
return json.loads(safe_serialize(return_dict))

View file

@ -0,0 +1,86 @@
""" API Backend - Base Resource Models """
from functools import wraps
import logging
import re
from flask_restful import Resource, abort
from cryptoadvance.specter.api import api_rest
from cryptoadvance.specter.api.security import require_admin
from cryptoadvance.specter.api import auth
from cryptoadvance.specter.specter_error import SpecterError
logger = logging.getLogger(__name__)
def error_handling(func):
"""User needs Admin-rights method decorator"""
@wraps(func)
def wrapper(*args, **kwargs):
"""wrapping the whole error-handling around methods"""
try:
return func(*args, **kwargs)
except SpecterError as se:
logger.error(se)
# Not that elegant as this function is accumulating all the error-handling for different
# endpoints. On the other hand, this is probabyl more tied to different SpecterErrors
# rather than the implementation of a rest-endpoint.
match = re.match("Wallet (.+) does not exist!", str(se))
if match:
return abort(403, message=f"Wallet {match.group(1)} does not exist")
if str(se).endswith(
"does not have sufficient funds to make the transaction."
):
return abort(
412,
message=f"Wallet does not have sufficient funds to make the transaction.",
)
return abort(500)
except Exception as e:
logger.error("Unexpected Exception in Rest-Request:")
logger.exception(e)
return abort(
500,
message="Can't tell you the reason of the issue. Please check the logs",
)
return wrapper
class BaseResource(Resource):
"""A baseClass for rsources which returns Method not allowed by default"""
method_decorators = [error_handling]
def get(self, *args, **kwargs):
abort(405)
def post(self, *args, **kwargs):
abort(405)
def put(self, *args, **kwargs):
abort(405)
def patch(self, *args, **kwargs):
abort(405)
def delete(self, *args, **kwargs):
abort(405)
class SecureResource(BaseResource):
"""A REST-resource which makes sure that the user is Authenticated"""
method_decorators = [error_handling, auth.login_required]
class AdminResource(BaseResource):
"""A REST-resource which makes sure that the user is an admin"""
method_decorators = [error_handling, require_admin, auth.login_required]
def rest_resource(resource_cls):
"""Decorator for adding resources to Api App"""
api_rest.add_resource(resource_cls, *resource_cls.endpoints)

View file

@ -0,0 +1,42 @@
"""
liveness and readiness probes are a semi-standard way of health-checking.
See e.g. here:
https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
"""
import json
import logging
from os import abort
from cryptoadvance.specter.api.rest.base import BaseResource, rest_resource
from flask import current_app as app
logger = logging.getLogger(__name__)
@rest_resource
class ResourceLiveness(BaseResource):
"""/api/healthz/liveness
Whether the app is up and running although it might not have connection to DB/nodes etc.
"""
endpoints = ["/healthz/liveness"]
def get(self):
return {"message": "i am alive"}
@rest_resource
class ResourceReadyness(BaseResource):
"""/api/healthz/readyness
Whether the app is up and running AND ALL its dependent services (in our case nodes) are properly functioning as well.
"""
endpoints = ["/healthz/readyness"]
def get(self):
try:
# Not sure whether that's enough. Probably improvable:
app.specter.check()
except Exception as e:
abort(500, message="Readyness probe failed")
return {"message": "i am ready"}

View file

@ -0,0 +1,41 @@
import logging
from .base import (
SecureResource,
rest_resource,
)
from flask import current_app as app, request
from ...wallet import Wallet
from ...util.psbt_creator import PsbtCreator
from .. import auth
logger = logging.getLogger(__name__)
@rest_resource
class ResourcePsbt(SecureResource):
"""/api/v1alpha/specter"""
endpoints = ["/v1alpha/wallets/<wallet_alias>/psbt"]
def get(self, wallet_alias):
# ToDo: check whether the user has access to the wallet
user = auth.current_user()
wallet: Wallet = app.specter.user_manager.get_user(
user
).wallet_manager.get_by_alias(wallet_alias)
pending_psbts = wallet.pending_psbts
return {"result": pending_psbts or []}
def post(self, wallet_alias):
user = auth.current_user()
wallet: Wallet = app.specter.user_manager.get_user(
user
).wallet_manager.get_by_alias(wallet_alias)
logger.debug(f"Got a post request for creating a psbt: {request.json}")
psbt_creator = PsbtCreator(
app.specter, wallet, "json", request_json=request.json
)
psbt_creator.create_psbt(wallet)
return {"result": psbt_creator.psbt}

View file

@ -0,0 +1,46 @@
import logging
from cryptoadvance.specter.api.rest.base import (
AdminResource,
rest_resource,
)
from flask import current_app as app
from datetime import datetime
logger = logging.getLogger(__name__)
@rest_resource
class ResourceSpecter(AdminResource):
"""/api/v1alpha/specter"""
endpoints = ["/v1alpha/specter"]
def get(self):
specter_data = app.specter
return_dict = {
"data_folder": specter_data.data_folder,
"config": specter_data.config,
"info": specter_data.info,
"network_info": specter_data.network_info,
"device_manager_datafolder": specter_data.device_manager.data_folder,
"devices_names": specter_data.device_manager.devices_names,
"wallets_names": specter_data.wallet_manager.wallets_names,
"last_update": datetime.now().strftime("%m/%d/%Y, %H:%M:%S"),
}
# Include alias list for easy lookup between names and alias
# Maybe there's a better way to create this
wallets_alias = []
alias_name = {}
name_alias = {}
for wallet in return_dict["wallets_names"]:
alias = specter_data.wallet_manager.wallets[wallet].alias
wallets_alias.append(alias)
alias_name[alias] = wallet
name_alias[wallet] = alias
return_dict["alias_name"] = alias_name
return_dict["name_alias"] = name_alias
return_dict["wallets_alias"] = wallets_alias
return return_dict

View file

@ -0,0 +1,34 @@
import json
import logging
from cryptoadvance.specter.api.rest.base import AdminResource, rest_resource
from flask import current_app as app
from .. import auth
logger = logging.getLogger(__name__)
@rest_resource
class ResourceTXlist(AdminResource):
"""/api/v1alpha/full_txlist"""
endpoints = ["/v1alpha/specter/full_txlist/"]
def get(self):
user = auth.current_user()
wallet_manager = app.specter.user_manager.get_user(user).wallet_manager
validate_merkle_proofs = app.specter.config.get("validate_merkle_proofs")
idx = 0
tx_len = 1
tx_list = []
transactions = wallet_manager.full_txlist(
fetch_transactions=False, validate_merkle_proofs=validate_merkle_proofs
)
tx_list.append(transactions)
# Flatten the list
flat_list = []
for element in tx_list:
for dic_item in element:
flat_list.append(dic_item)
return flat_list

View file

@ -0,0 +1,52 @@
""" Security Related things for the REST-API """
import logging
from functools import wraps
from cryptoadvance.specter.user import User, verify_password as user_verify_password
from flask import current_app as app
from flask import g
from flask_restful import abort
# from flask_httpauth import HTTPBasicAuth
from . import auth
logger = logging.getLogger(__name__)
# auth = HTTPBasicAuth()
@auth.verify_password
def verify_password(username, password):
"""Validate user passwords and store user in the 'g' object"""
if not username or not password:
return abort(401)
the_user = app.specter.user_manager.get_user_by_username(username)
if not the_user:
return abort(401)
g.user = app.specter.user_manager.get_user_by_username(username)
if user_verify_password(g.user.password, password):
logger.info(f"Rest-Request for user {username} PASSED password-test")
return username
else:
logger.info(f"Rest-Request for user {username} FAILED password-test")
return abort(401)
return g.user is not None and verify_password(g.user.password, password)
def require_admin(func):
"""User needs Admin-rights method decorator"""
@wraps(func)
def wrapper(*args, **kwargs):
"""this needs to get implemented properly"""
# Verify if User is Admin
if g.user == None:
return abort(401)
if g.user.is_admin:
return func(*args, **kwargs)
else:
return abort(401)
return wrapper

View file

@ -0,0 +1,18 @@
<html>
<body>
The documentation for the API FOR THE MASTER BRANCH can be found here:
<a href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/api/README.md">https://github.com/cryptoadvance/specter-desktop/blob/master/docs/api/api.md</a>
<br><br>
Make sure to choose the right version for the version you're currently using: {{version}}
<br><br>
{% if version != "custom" %}
So assuming that, please use this url:
<a href="https://github.com/cryptoadvance/specter-desktop/blob/v{{version}}/docs/api/README.md">https://github.com/cryptoadvance/specter-desktop/blob/v{{version}}/docs/api/api.md</a>
{% endif %}
</body>
</html>

View file

@ -0,0 +1,11 @@
from flask import render_template
from cryptoadvance.specter.api import api_bp
from flask import current_app as app
@api_bp.route("/")
def api():
"""rendering the documentation for the api"""
return render_template(
"api.html", version=app.specter.version.get_current_version()
)

View file

@ -38,6 +38,7 @@ class BaseConfig(object):
SPECTER_DATA_FOLDER = os.path.expanduser(
os.getenv("SPECTER_DATA_FOLDER", "~/.specter")
)
SPECTER_API_ACTIVE = _get_bool_env_var("SPECTER_API_ACTIVE", "False")
# Logging
# SPECTER_LOGFILE will get created dynamically in server.py
# using:
@ -129,6 +130,8 @@ class DevelopmentConfig(BaseConfig):
SPECTER_DATA_FOLDER = os.path.expanduser(
os.getenv("SPECTER_DATA_FOLDER", "~/.specter_dev")
)
# API active by default in dev-mode
SPECTER_API_ACTIVE = _get_bool_env_var("SPECTER_API_ACTIVE", "True")
# Env vars take priority over config settings so ensure that this is set
os.environ["FLASK_ENV"] = "development"
@ -136,11 +139,18 @@ class DevelopmentConfig(BaseConfig):
class TestConfig(BaseConfig):
SECRET_KEY = "test key"
# This should never be used as the data-folder is injected at runtime
# But let's be sure before something horrible happens:
SPECTER_DATA_FOLDER = os.path.expanduser(
os.getenv("SPECTER_DATA_FOLDER", "~/.specter_testing")
)
# API active by default in test-mode
SPECTER_API_ACTIVE = _get_bool_env_var("SPECTER_API_ACTIVE", "True")
class CypressTestConfig(TestConfig):
SPECTER_DATA_FOLDER = os.path.expanduser(
os.getenv("SPECTER_DATA_FOLDER", "~/.specter-cypress")
os.getenv("SPECTER_DATA_FOLDER", "~/.specter_cypress")
)
PORT = os.getenv("PORT", 25444)

View file

@ -1,14 +1,10 @@
import json
import logging
import os
import psutil
from .helpers import is_testnet
from .specter_error import SpecterError, ExtProcTimeoutException
from .rpc import (
BitcoinRPC,
RpcError,
autodetect_rpc_confs,
detect_rpc_confs,
get_default_datadir,
)
from .process_controller.bitcoind_controller import BitcoindPlainController
@ -23,6 +19,11 @@ class InternalNode(Node):
So it has start and stop methods and one called is_bitcoind_running
"""
# Possible Stati
BROKEN = "Broken"
DOWN = "Down"
RUNNING = "Running"
def __init__(
self,
name,
@ -58,8 +59,8 @@ class InternalNode(Node):
self.bitcoind_path = bitcoind_path
self.bitcoind_network = bitcoind_network
self._bitcoind = None
self.bitcoin_pid = False
self.version = version
self._status = self.DOWN
if self.bitcoind_network != "main":
if self.bitcoind_network == "testnet" and not self.datadir.endswith(
"/testnet3"
@ -119,29 +120,48 @@ class InternalNode(Node):
return node_json
def start(self, timeout=15):
"""Failsafe way to start an internal node."""
potential_bitcoind_process = BitcoindProcess.by_password(self.password)
if potential_bitcoind_process:
logger.info(
f"Skipping start of internal node, existing one found {potential_bitcoind_process}"
)
self.bitcoind.attach_to_proc_id(potential_bitcoind_process)
self._status = self.RUNNING
return self.update_rpc()
try:
logger.info(f"STARTING bitcoind {self.name} from status {self.status}")
self.bitcoind.start_bitcoind(
datadir=os.path.expanduser(self.datadir),
timeout=timeout, # At the initial startup, we don't wait on bitcoind
)
self._status = self.RUNNING
except ExtProcTimeoutException as e:
logger.error(e)
e.check_logfile(os.path.join(self.datadir, "debug.log"))
self._status = self.BROKEN
logger.error(e.get_logger_friendly())
except SpecterError as e:
self._status = self.BROKEN
logger.error(e)
# Likely files of bitcoind were not found. Maybe deleted by the user?
finally:
try:
self.bitcoin_pid = self.bitcoind.node_proc.pid
except Exception as e:
logger.error(e)
logger.info(f"STARTUP process complete {self.name} to status {self.status}")
return self.update_rpc()
def _update_status(self, bitcoin, status):
self._bitcoin = bitcoin
self._status = status
def stop(self):
if self._bitcoind:
self._bitcoind.stop_bitcoind()
self.bitcoin_pid = False
logger.info(f"STOPPING bitcoind {self.name} from status {self.status}")
success = self._bitcoind.stop_bitcoind()
if success:
self._update_status(None, self.DOWN)
logger.info(f"bitcoind {self.name} stopped")
else:
logger.error(f"Failed to stop bitcoind {self.name}")
self._status = self.BROKEN
logger.info(f"STOPPING process complete {self.name} to status {self.status}")
@property
def bitcoind(self):
@ -159,5 +179,88 @@ class InternalNode(Node):
"Bitcoin Core files missing. Make sure Bitcoin Core is installed within Specter"
)
@property
def status(self):
"""RUNNING DOWN or BROKEN."""
return self._status
def is_bitcoind_running(self):
return self._bitcoind and self._bitcoind.check_existing()
if self._status == self.RUNNING:
return self._bitcoind and self._bitcoind.check_existing()
return False
@classmethod
def kill_process(cls, proc_id):
try:
proc = psutil.Process(proc_id)
proc.terminate()
# os.kill(proc_id, signal.SIGTERM)
except ProcessLookupError:
logger.error("Process with ID {proc_id} does not exist")
class BitcoindProcess:
"""This is a class which represents a Bitcoind-process which is detected out of the context it has been created
it wraps a Process and therefore behaves like one. At least until it doesn't
"""
def __init__(self, pid):
self.pid = pid
self.proc = psutil.Process(self.pid)
@classmethod
def by_password(cls, password):
"""returns a BitcoindProcess which fits the rpcpassword == password"""
pid = cls.get_process_id(password)
if not pid is None:
return BitcoindProcess(pid)
return None
def terminate(self):
try:
self.proc.terminate()
except ProcessLookupError:
logger.error("Process with ID {proc_id} does not exist")
def kill(self):
self.proc.kill()
def poll(self):
return self.proc.poll()
def get_cmd_arg_value(self, arg_key):
"""parses the cmdline of the bitcoind-cmd and returns the value to the corresponding key
e.g. for -rpcpassword=secret
get_cmd_arg_value("rpcpassword") == "secret"
"""
for cmd_arg in self.proc.cmdline():
if cmd_arg.startswith(f"-{arg_key}"):
arg_value = cmd_arg.split("=")[1]
return arg_value
def __repr__(self) -> str:
return f"<BitcoindProcess datadir={self.get_cmd_arg_value('datadir')} rpcport={self.get_cmd_arg_value('rpcport')} >"
@classmethod
def get_process_id(cls, searched_password):
for proc in psutil.process_iter():
# logger.debug(f"investigating {proc.name()}")
try:
# Get process name & pid from process object.
if not proc.name().endswith("bitcoind"):
continue
if not proc.exe().endswith("/bitcoin-binaries/bin/bitcoind"):
continue
password = None
for cmd_arg in proc.cmdline():
if cmd_arg.startswith("-rpcpassword"):
password = cmd_arg.split("=")[1]
if searched_password == password:
logger.debug(
f"Found internal_node bitcoind process with the password given: {proc.pid}"
)
return proc.pid
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
# processes are volatile
pass
return None

View file

@ -321,6 +321,7 @@ class WalletManager:
# get Wallet class instance
if w:
self.wallets[name] = w
logger.info(f"Successfully created Wallet {name}")
return w
else:
raise ("Failed to create new wallet")

View file

@ -226,8 +226,6 @@ class Node:
if self.rpc and self.rpc.test_connection():
logger.info(f"persisting {self} in update_rpc")
write_node(self, self.fullpath)
else:
logger.error(f"not persisting broken {self.rpc} in update_rpc")
self.check_info()
return False if not self.rpc else self.rpc.test_connection()
@ -284,7 +282,7 @@ class Node:
if self.rpc is None:
logger.error(f"connection of {self} is None in check_info")
elif not self.rpc.test_connection():
logger.error(
logger.debug(
f"connection {self.rpc} failed test_connection in check_info:"
)
try:

View file

@ -1,5 +1,7 @@
from .node_controller import NodePlainController
import atexit
import logging
import signal
logger = logging.getLogger(__name__)
@ -48,8 +50,33 @@ class BitcoindPlainController(NodePlainController):
timeout,
)
def attach_to_proc_id(self, bitcoind_process):
"""This assumes that the calling context (prob. a internal_node instance) found a process which is suited.
So instead of starting, we're somehow faking a start.
This behaviour should be explicitely triggered as the management of that should be duty of InternalNode
"""
# avoid circular reference
from ..internal_node import BitcoindProcess
self.datadir = bitcoind_process.get_cmd_arg_value("datadir")
self.node_proc = bitcoind_process
self.cleanup_hard = (
False # assuming an internal node which should not get killed -9
)
def cleanup_node_callback(signal_number=None, stack=None):
self.cleanup_node(False, self.datadir)
atexit.register(cleanup_node_callback)
# This is for CTRL-C --> SIGINT
signal.signal(signal.SIGINT, cleanup_node_callback)
# This is for kill $pid --> SIGTERM
signal.signal(signal.SIGTERM, cleanup_node_callback)
self.status = "Running"
def stop_bitcoind(self):
self.stop_node()
return self.stop_node()
def version(self):
"""Returns the version of bitcoind, e.g. "v0.19.1" """

View file

@ -1,25 +1,26 @@
""" Stuff to control a bitcoind-instance.
"""
import atexit
import json
import logging
import os
import signal
import psutil
import platform
import re
import shutil
import signal
import subprocess
import tempfile
import time
import json
import platform
from ..util.shell import which, get_last_lines_from_file
from ..rpc import RpcError
from ..rpc import BitcoinRPC
from ..helpers import load_jsons
from ..specter_error import ExtProcTimeoutException, SpecterError
from urllib3.exceptions import NewConnectionError, MaxRetryError
import psutil
from cryptoadvance.specter.liquid.rpc import LiquidRPC
from requests.exceptions import ConnectionError
from urllib3.exceptions import MaxRetryError, NewConnectionError
from ..helpers import load_jsons
from ..rpc import BitcoinRPC, RpcError
from ..specter_error import ExtProcTimeoutException, SpecterError
from ..util.shell import get_last_lines_from_file, which
logger = logging.getLogger(__name__)
@ -28,8 +29,14 @@ class Btcd_conn:
"""An object to easily store connection data to bitcoind-comatible nodes (Bitcoin/Elements)"""
def __init__(
self, rpcuser="bitcoin", rpcpassword="secret", rpcport=18543, ipaddress=None
self,
node_impl="bitcoin",
rpcuser="bitcoin",
rpcpassword="secret",
rpcport=18543,
ipaddress=None,
):
self.node_impl = node_impl
self.rpcport = rpcport
self.rpcuser = rpcuser
self.rpcpassword = rpcpassword
@ -48,10 +55,16 @@ class Btcd_conn:
def get_rpc(self):
"""returns a BitcoinRPC"""
# def __init__(self, user, passwd, host="127.0.0.1", port=8332, protocol="http", path="", timeout=30, **kwargs):
rpc = BitcoinRPC(
self.rpcuser, self.rpcpassword, host=self.ipaddress, port=self.rpcport
)
if self.node_impl == "bitcoin":
rpc = BitcoinRPC(
self.rpcuser, self.rpcpassword, host=self.ipaddress, port=self.rpcport
)
elif self.node_impl == "elements":
rpc = LiquidRPC(
self.rpcuser, self.rpcpassword, host=self.ipaddress, port=self.rpcport
)
else:
raise SpecterError(f"Unknown node_impl: {self.node_impl}")
rpc.getblockchaininfo()
return rpc
@ -96,10 +109,15 @@ class NodeController:
):
try:
self.rpcconn = Btcd_conn(
rpcuser=rpcuser, rpcpassword=rpcpassword, rpcport=rpcport
node_impl=node_impl,
rpcuser=rpcuser,
rpcpassword=rpcpassword,
rpcport=rpcport,
)
self.network = network
self.node_impl = node_impl
# reasonable default
self.cleanup_hard = False
except Exception as e:
logger.exception(f"Failed to instantiate BitcoindController. Error: {e}")
raise e
@ -118,11 +136,12 @@ class NodeController:
if bitcoind_path == docker, it'll run bitcoind via docker.
Specify a longer timeout for slower devices (e.g. Raspberry Pi)
"""
if self.check_existing() != None:
logger.warning(f"Reusing existing {self.node_impl}d")
return self.rpcconn
if not self.check_existing() is None:
# This should not happen. For bitcoind, have a look in BitcoindController.attach_to_proc_id()
raise SpecterError(
f"While starting Node, there is already a Node running at {self.rpcconn.render_url(password_mask=True)}"
)
logger.debug(f"Starting {self.node_impl}d")
self._start_node(
cleanup_at_exit,
cleanup_hard=cleanup_hard,
@ -142,7 +161,7 @@ class NodeController:
except Exception as e:
self.status = "Error"
raise e
logger.info(f"Successfully started {self.node_impl}d in {self.datadir}")
if "" not in self.get_rpc().listwallets():
logger.info("Creating Default-wallet")
self.get_rpc().createwallet("", False, False, "", False, True, True)
@ -231,8 +250,11 @@ class NodeController:
default_address = default_rpc.getaddressinfo(default_address)[
"unconfidential"
]
if balance < amount:
while True:
btc_balance = default_rpc.getbalance()
rpc.generatetoaddress(102, default_address)
if btc_balance > amount:
break
default_rpc.sendtoaddress(address, amount)
@staticmethod
@ -381,47 +403,68 @@ class NodePlainController(NodeController):
)
time.sleep(0.2) # sleep 200ms (catch stdout of stupid errors)
if not self.node_proc.poll() is None:
raise SpecterError(f"Could not start node due to:" + self.get_debug_log())
# Might itself raise a SpecterError:
debug_logs = self.get_debug_log()
raise SpecterError(f"Could not start node due to:" + debug_logs)
logger.debug(
f"Running {self.node_impl}d-process with pid {self.node_proc.pid} in datadir {datadir}"
)
# This function is redirecting to the class.member as it needs a fixed parameterlist: (signal_number, stack)
def cleanup_node(signal_number=None, stack=None):
def cleanup_node_callback(signal_number=None, stack=None):
self.cleanup_node(cleanup_hard, datadir)
# If the node is shutdown via self.stop_node() (e.g. pytests) we need to know how hard we should do that
self.cleanup_hard = cleanup_hard
if cleanup_at_exit:
logger.info(
"Register function cleanup_node for atexit, SIGINT, and SIGTERM"
)
atexit.register(cleanup_node)
atexit.register(cleanup_node_callback)
# This is for CTRL-C --> SIGINT
signal.signal(signal.SIGINT, cleanup_node)
signal.signal(signal.SIGINT, cleanup_node_callback)
# This is for kill $pid --> SIGTERM
signal.signal(signal.SIGTERM, cleanup_node)
signal.signal(signal.SIGTERM, cleanup_node_callback)
def get_debug_log(self):
logfile_location = os.path.join(
self.datadir,
self.network if self.network != "testnet" else "testnet3",
"debug.log",
)
try:
logfile_location = os.path.join(
self.datadir,
self.network if self.network != "testnet" else "testnet3",
"debug.log",
)
return "".join(get_last_lines_from_file(logfile_location))
except FileNotFoundError as e:
raise SpecterError(
f"Could not find debug.log at {logfile_location}. Is that directory even existing?"
)
except Exception as e:
logger.exception(f"Failed to get debug logs. Error: {e}")
return ""
return "[Failed to get debug logs. Check the logs for details]"
def cleanup_node(self, cleanup_hard=None, datadir=None):
"""KILLS or TERMINATES the node-process depending on cleanup_hard
removes the datadir in case of KILL
"""
returnvalue = True # assume only the best
if not hasattr(self, "datadir"):
self.datadir = None
if cleanup_hard == None:
cleanup_hard = self.cleanup_hard
if not hasattr(self, "node_proc"):
logger.info("node process was not running")
if cleanup_hard:
logger.info(f"Removing node datadir: {datadir}")
if datadir is None:
datadir = self.datadir
shutil.rmtree(datadir, ignore_errors=True)
return
returnvalue = False
timeout = 50 # in secs
logger.info(
f"Cleaning up (signal:{cleanup_hard} (sig_int: {signal.SIGINT}), datadir:{datadir})"
f"Cleaning up (cleanup_hard:{cleanup_hard} , datadir:{self.datadir})"
)
if cleanup_hard:
try:
@ -431,32 +474,38 @@ class NodePlainController(NodeController):
)
shutil.rmtree(self.datadir, ignore_errors=True)
except Exception as e:
logger.debug(e)
logger.error(e)
returnvalue = False
else:
try:
self.node_proc.terminate() # might take a bit longer than kill but it'll preserve block-height
logger.info(
f"Terminated {self.node_impl}d with pid {self.node_proc.pid}, waiting for termination (timeout {timeout} secs)..."
)
# self.node_proc.wait() # doesn't have a timeout
procs = psutil.Process().children()
for p in procs:
p.terminate()
_, alive = psutil.wait_procs(procs, timeout=timeout)
for p in alive:
if not hasattr(self, "node_proc"):
returnvalue = False
else:
self.node_proc.terminate() # might take a bit longer than kill but it'll preserve block-height
logger.info(
f"{self.node_impl} did not terminated in time, killing!"
f"Terminated {self.node_impl}d with pid {self.node_proc.pid}, waiting for termination (timeout {timeout} secs)..."
)
p.kill()
# self.node_proc.wait() # doesn't have a timeout
procs = psutil.Process().children()
for p in procs:
p.terminate()
_, alive = psutil.wait_procs(procs, timeout=timeout)
for p in alive:
logger.info(
f"{self.node_impl} did not terminated in time, killing!"
)
p.kill()
except ProcessLookupError:
# Bitcoind probably never came up or crashed. Silently ignored
pass
# Bitcoind probably never came up or crashed.
returnvalue = False
if platform.system() == "Windows":
subprocess.run("Taskkill /IM bitcoind.exe /F")
return returnvalue
def stop_node(self):
self.cleanup_node()
success = self.cleanup_node()
self.status = "Down"
return success
def check_existing(self):
"""other then in docker, we won't check on the "instance-level". This will return true if a

View file

@ -160,6 +160,12 @@ def init_app(app, hwibridge=False, specter=None):
def index():
return redirect(url_for("hwi_server.hwi_bridge_settings"))
if app.config["SPECTER_API_ACTIVE"]:
app.logger.info("Initializing REST ...")
from cryptoadvance.specter.api import api_bp
app.register_blueprint(api_bp)
@app.context_processor
def inject_tor():
if app.config["DEBUG"]:

View file

@ -179,7 +179,7 @@
}
showError(jsonResponse.error, 4000);
} catch(e) {
showError('{{ _("Failed to start Bitcoin Core...") }}';
showError('{{ _("Failed to start Bitcoin Core...") }}');
showError(e);
}
}

View file

@ -54,6 +54,20 @@ class User(UserMixin):
return ""
return f"_{self.id}"
@property
def password(self):
return self._password
@password.setter
def password(self, value):
"""pass a json or a plain-password here"""
try:
if value.get("salt") and value.get("pwdhash"):
self._password = value
except:
salted_hashed_password = hash_password(value)
self._password = salted_hashed_password
@classmethod
def from_json(cls, user_dict, specter):
# TODO: Unify admin in backwards compatible way
@ -75,8 +89,8 @@ class User(UserMixin):
specter,
is_admin=True,
)
except:
raise SpecterError("Unable to parse user JSON.")
except Exception as e:
raise SpecterError(f"Unable to parse user JSON.:{e}")
@property
def json(self):
@ -211,6 +225,8 @@ class User(UserMixin):
self.save_info(delete=True)
def __eq__(self, other):
if other == None:
return False
if isinstance(other, str):
return self.id == other
return self.id == other.id

View file

@ -1,4 +1,5 @@
import json
from json.decoder import JSONDecodeError
import logging
from math import isnan
@ -22,8 +23,7 @@ class PsbtCreator:
request_form=None,
recipients_txt=None,
recipients_amount_unit=None,
substract=None,
substract_from=1,
request_json=None,
):
"""
* depending of ui_option = (ui|text) Fill the payment-details in either of these:
@ -50,6 +50,10 @@ class PsbtCreator:
specter, wallet, request_form=request_form
)
elif ui_option == "text":
if recipients_txt is None or recipients_amount_unit is None:
raise SpecterError(
"recipients_txt and recipients_amount_unit is mandatory"
)
(
self.addresses,
self.labels,
@ -61,6 +65,21 @@ class PsbtCreator:
recipients_txt=recipients_txt,
recipients_amount_unit=recipients_amount_unit,
)
elif ui_option == "json":
if request_json is None:
raise SpecterError("request_json is mandatory")
(
self.addresses,
self.labels,
self.amounts,
self.amount_units,
) = PsbtCreator.paymentinfo_from_json(
specter, wallet, request_json=request_json
)
else:
raise SpecterError(
f"Unknown ui_option: {ui_option}. Valid ones are ui|text|json"
)
# normalizing
self.addresses = [
address.lower()
@ -69,7 +88,10 @@ class PsbtCreator:
for address in self.addresses
]
# get kwargs
self.kwargs = PsbtCreator.kwargs_from_request_form(request_form)
if ui_option == "ui" or ui_option == "text":
self.kwargs = PsbtCreator.kwargs_from_request_form(request_form)
elif ui_option == "json":
self.kwargs = PsbtCreator.kwargs_from_request_json(request_json)
if specter.is_liquid:
self.kwargs["assets"] = self.amount_units
@ -77,24 +99,20 @@ class PsbtCreator:
"""creates the PSBT via the wallet and modifies it for if substract is true
If there was a "estimate_fee" in the request_form, the PSBT will not get persisted
"""
try:
self.psbt = wallet.createpsbt(self.addresses, self.amounts, **self.kwargs)
if self.psbt is None:
raise SpecterError(
"Probably you don't have enough funds, or something else..."
)
else:
# calculate new amount if we need to subtract
if self.kwargs["subtract"]:
for v in self.psbt["tx"]["vout"]:
if self.addresses[0] in v["scriptPubKey"].get(
"addresses", [""]
) or self.addresses[0] == v["scriptPubKey"].get("address", ""):
self.amounts[0] = v["value"]
return self.psbt
except Exception as e:
logger.exception(e)
raise SpecterError(f"{e} ... check the logs for the stacktrace")
self.psbt = wallet.createpsbt(self.addresses, self.amounts, **self.kwargs)
if self.psbt is None:
raise SpecterError(
"Probably you don't have enough funds, or something else..."
)
else:
# calculate new amount if we need to subtract
if self.kwargs["subtract"]:
for v in self.psbt["tx"]["vout"]:
if self.addresses[0] in v["scriptPubKey"].get(
"addresses", [""]
) or self.addresses[0] == v["scriptPubKey"].get("address", ""):
self.amounts[0] = v["value"]
return self.psbt
@classmethod
def paymentinfo_from_ui(cls, specter, wallet, request_form):
@ -139,16 +157,88 @@ class PsbtCreator:
amounts = []
amount_units = []
for output in recipients_txt.splitlines():
addresses.append(output.split(",")[0].strip())
if recipients_amount_unit == "sat":
amounts.append(float(output.split(",")[1].strip()) / 1e8)
elif recipients_amount_unit == "btc":
amounts.append(float(output.split(",")[1].strip()))
try:
if output.isspace() or output == "":
continue
addresses.append(output.split(",")[0].strip())
if recipients_amount_unit == "sat":
amounts.append(float(output.split(",")[1].strip()) / 1e8)
elif recipients_amount_unit == "btc":
amounts.append(float(output.split(",")[1].strip()))
else:
raise SpecterError(
f"Unknown recipients_amount_unit: {recipients_amount_unit}"
)
labels.append("")
amount_units.append(recipients_amount_unit)
except IndexError as ie:
logger.error(f"line does not match expected pattern: '{output}'")
return addresses, labels, amounts, amount_units
@classmethod
def paymentinfo_from_json(cls, specter, wallet, request_json):
"""calculates the correct format needed by wallet.createpsbt() out of a json
Example:
{
"recipients" : [
{
"address": "BCRT1qgc6h85z43g3ss2dl5zdrzrp3ef6av4neqcqhh8",
"amount": 0.1,
"unit": "btc",
"label": "someLabel"
},
{
"address": "bcrt1q3kfetuxpxvujasww6xas94nawklvpz0e52uw8a",
"amount": 111211,
"unit": "sat",
"label": "someOtherLabel"
}
],
"rbf_tx_id": "",
"subtract_from": "1",
"fee_rate": "64",
"rbf": true,
}
returns something like (addresses, labels, amounts, amount_units) (all arrays)
"""
addresses = []
labels = []
amounts = []
amount_units = []
try:
if isinstance(request_json, dict):
json_data = request_json
else:
raise SpecterError(
f"Unknown recipients_amount_unit: {recipients_amount_unit}"
)
labels.append("")
json_data = json.loads(request_json)
except JSONDecodeError as e:
raise SpecterError(f"Error parsing json: {e}")
for recipient in json_data["recipients"]:
try:
addresses.append(recipient["address"])
try:
amount = float(recipient["amount"])
if recipient["unit"] == "sat":
amounts.append(float(amount / 1e8))
elif recipient["unit"] == "btc":
amounts.append(amount)
else:
raise SpecterError(
f"Non-compliant json: Unknown unit {recipient['unit']}"
)
except ValueError as e:
raise SpecterError(
f"Could not parse amount {recipient.get('amount')} because {e}"
)
unit = recipient["unit"]
amount_units.append(unit)
label = recipient.get("label", "")
labels.append(label)
if label != "":
wallet.setlabel(recipient["address"], label)
except KeyError as ke:
raise SpecterError(f"Data missing in json: {ke}")
return addresses, labels, amounts, amount_units
def kwargs_from_request_form(request_form):
@ -182,3 +272,36 @@ class PsbtCreator:
"rbf_edit_mode": (rbf_tx_id != ""),
}
return kwargs
@classmethod
def kwargs_from_request_json(cls, request_json):
"""calculates the needed kwargs fow wallet.createpsbt() out of a request_json"""
# Who pays the fees?
try:
if isinstance(request_json, dict):
json_data = request_json
else:
json_data = json.loads(request_json)
except JSONDecodeError as e:
raise SpecterError(f"Error parsing json: {e}")
subtract = bool(json_data.get("subtract", False))
subtract_from = int(json_data.get("subtract_from", 1))
fee_rate = float(json_data.get("fee_rate", None))
rbf = bool(json_data.get("rbf", False))
rbf_tx_id = json_data.get("rbf_tx_id", "")
kwargs = {
"subtract": subtract,
"subtract_from": subtract_from - 1,
"fee_rate": fee_rate,
"rbf": rbf,
"selected_coins": [],
"readonly": False, # determines whether the psbt gets persisted
"rbf_edit_mode": (rbf_tx_id != ""),
}
return kwargs
def __repr__(self) -> str:
status = "created" if hasattr(self, "psbt") else "initialized"
return f"<{self.__class__.__name__} amountSum={sum(self.amounts) } {status}>"

View file

@ -1,6 +1,7 @@
import json
import logging
import requests
from cryptoadvance.specter.managers.wallet_manager import WalletManager
from cryptoadvance.specter.specter_error import SpecterError
@ -13,7 +14,7 @@ logger = logging.getLogger(__name__)
class WalletImporter:
"""A class to create Wallets easily by json"""
def __init__(self, wallet_json, specter):
def __init__(self, wallet_json, specter, device_manager=None):
"""this will analyze the wallet_json and specifies self. ...:
* wallet_name
* recv_descriptor
@ -26,6 +27,8 @@ class WalletImporter:
* unknown_cosigners
* unknown_cosigners_types
"""
if device_manager is None:
device_manager = specter.device_manager
try:
self.wallet_data = json.loads(wallet_json)
(
@ -52,9 +55,7 @@ class WalletImporter:
self.cosigners,
self.unknown_cosigners,
self.unknown_cosigners_types,
) = self.descriptor.parse_signers(
specter.device_manager.devices, self.cosigners_types
)
) = self.descriptor.parse_signers(device_manager.devices, self.cosigners_types)
self.wallet_type = "multisig" if self.descriptor.multisig_N > 1 else "simple"
def create_nonexisting_signers(self, device_manager, request_form):
@ -71,11 +72,13 @@ class WalletImporter:
unknown_cosigner_type = request_form.get(
"unknown_cosigner_{}_type".format(i), "other"
)
device = device_manager.add_device(
name=unknown_cosigner_name,
device_type=unknown_cosigner_type,
keys=[unknown_cosigner_key],
)
logger.info(f"Creating device {device}")
self.keys.append(unknown_cosigner_key)
self.cosigners.append(device)
@ -93,9 +96,11 @@ class WalletImporter:
)
except Exception as e:
raise SpecterError(f"Failed to create wallet: {e}")
logger.info(f"Created Wallet {self.wallet}")
self.wallet.keypoolrefill(0, self.wallet.IMPORT_KEYPOOL, change=False)
self.wallet.keypoolrefill(0, self.wallet.IMPORT_KEYPOOL, change=True)
self.wallet.import_labels(self.wallet_data.get("labels", {}))
return self.wallet
def rescan_as_needed(self, specter):
"""will rescan the created wallet"""
@ -185,7 +190,7 @@ class WalletImporter:
wallet_name = wallet_data["keystore"]["label"]
if "xpub" in wallet_data["keystore"]:
wallet_type = wallet_type_by_slip132_xpub(
wallet_type = cls.wallet_type_by_slip132_xpub(
wallet_data["keystore"]["xpub"], is_multisig=False
)
else:

View file

@ -1264,13 +1264,8 @@ class Wallet:
available["trusted"] = round(available["trusted"], 8)
available["untrusted_pending"] = round(available["untrusted_pending"], 8)
balance["available"] = available
except:
balance = {
"trusted": 0,
"untrusted_pending": 0,
"immature": 0,
"available": {"trusted": 0, "untrusted_pending": 0},
}
except Exception as e:
raise SpecterError(f"was not able to get wallet_balance because {e}")
self.balance = balance
return self.balance
@ -1395,7 +1390,7 @@ class Wallet:
if not rbf_edit_mode:
if self.full_available_balance < sum(amounts):
raise SpecterError(
"The wallet does not have sufficient funds to make the transaction."
f"Wallet {self.name} does not have sufficient funds to make the transaction."
)
if selected_coins != []:
@ -1801,3 +1796,6 @@ class Wallet:
)
return addresses_info
def __repr__(self) -> str:
return f"<{self.__class__.__name__} name={self.name } alias={self.alias}>"

View file

@ -19,8 +19,11 @@ from cryptoadvance.specter.process_controller.bitcoind_docker_controller import
from cryptoadvance.specter.process_controller.elementsd_controller import (
ElementsPlainController,
)
from cryptoadvance.specter.rpc import BitcoinRPC
from cryptoadvance.specter.server import create_app, init_app
from cryptoadvance.specter.specter import Specter
from cryptoadvance.specter.user import User
from cryptoadvance.specter.util.wallet_importer import WalletImporter
pytest_plugins = ["ghost_machine"]
@ -50,9 +53,9 @@ def pytest_generate_tests(metafunc):
if "docker" in metafunc.fixturenames:
if metafunc.config.getoption("docker"):
# That's a list because we could do both (see above) but currently that doesn't make sense in that context
metafunc.parametrize("docker", [True], scope="module")
metafunc.parametrize("docker", [True], scope="session")
else:
metafunc.parametrize("docker", [False], scope="module")
metafunc.parametrize("docker", [False], scope="session")
def instantiate_bitcoind_controller(docker, request, rpcport=18543, extra_args=[]):
@ -82,6 +85,7 @@ def instantiate_bitcoind_controller(docker, request, rpcport=18543, extra_args=[
bitcoind_controller.start_bitcoind(
cleanup_at_exit=True, cleanup_hard=True, extra_args=extra_args
)
assert not bitcoind_controller.datadir is None
running_version = bitcoind_controller.version()
requested_version = request.config.getoption("--bitcoind-version")
assert running_version == requested_version, (
@ -107,6 +111,7 @@ def instantiate_elementsd_controller(request, rpcport=18643, extra_args=[]):
elementsd_controller.start_elementsd(
cleanup_at_exit=True, cleanup_hard=True, extra_args=extra_args
)
assert not elementsd_controller.datadir is None
running_version = elementsd_controller.version()
requested_version = request.config.getoption("--elementsd-version")
assert running_version == requested_version, (
@ -116,22 +121,31 @@ def instantiate_elementsd_controller(request, rpcport=18643, extra_args=[]):
return elementsd_controller
@pytest.fixture(scope="module")
@pytest.fixture(scope="session")
def bitcoin_regtest(docker, request):
bitcoind_regtest = instantiate_bitcoind_controller(docker, request, extra_args=None)
yield bitcoind_regtest
bitcoin_regtest: BitcoindPlainController.stop_bitcoind()
try:
assert bitcoind_regtest.get_rpc().test_connection()
assert not bitcoind_regtest.datadir is None
yield bitcoind_regtest
finally:
bitcoind_regtest.stop_bitcoind()
@pytest.fixture(scope="module")
@pytest.fixture(scope="session")
def elements_elreg(request):
return instantiate_elementsd_controller(request, extra_args=None)
elements_elreg = instantiate_elementsd_controller(request, extra_args=None)
try:
yield elements_elreg
assert not elements_elreg.datadir is None
finally:
elements_elreg.stop_elementsd()
@pytest.fixture
def empty_data_folder():
# Make sure that this folder never ever gets a reasonable non-testing use-case
with tempfile.TemporaryDirectory("_specter_home_tmp") as data_folder:
with tempfile.TemporaryDirectory(prefix="specter_home_tmp_") as data_folder:
yield data_folder
@ -275,13 +289,13 @@ def wallets_filled_data_folder(devices_filled_data_folder):
"change_keypool": 5,
"type": "simple",
"description": "Single (Segwit)",
"key": {
"keys": [{
"derivation": "m/84h/1h/0h",
"original": "vpub5Y35MNUT8sUR2SnRCU9A9S6z1JDACMTuNnM8WHXvuS7hCwuVuoRAWJGpi66Yo8evGPiecN26oLqx19xf57mqVQjiYb9hbb4QzbNmFfsS9ko",
"fingerprint": "1ef4e492",
"type": "wpkh",
"xpub": "tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc"
},
}],
"recv_descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/0/*)#xp8lv5nr",
"change_descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/1/*)#h4z73prm",
"device": "Trezor",
@ -301,7 +315,7 @@ def device_manager(devices_filled_data_folder):
@pytest.fixture
def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder):
# Make sure that this folder never ever gets a reasonable non-testing use-case
assert bitcoin_regtest.get_rpc().test_connection()
config = {
"rpc": {
"autodetect": False,
@ -317,15 +331,56 @@ def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder):
},
}
specter = Specter(data_folder=devices_filled_data_folder, config=config)
assert specter.chain == "regtest"
# Create a User
someuser: User = specter.user_manager.add_user(
User.from_json(
{
"id": "someuser",
"username": "someuser",
"password": "somepassword",
"config": {},
"is_admin": False,
},
specter,
)
)
specter.user_manager.save()
specter.check()
assert not someuser.wallet_manager.working_folder is None
# Create a Wallet
wallet_json = '{"label": "a_simple_wallet", "blockheight": 0, "descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/0/*)#xp8lv5nr", "devices": [{"type": "trezor", "label": "trezor"}]} '
wallet_importer = WalletImporter(
wallet_json, specter, device_manager=someuser.device_manager
)
wallet_importer.create_nonexisting_signers(
someuser.device_manager,
{"unknown_cosigner_0_name": "trezor", "unknown_cosigner_0_type": "trezor"},
)
dm: DeviceManager = someuser.device_manager
wallet = wallet_importer.create_wallet(someuser.wallet_manager)
# fund it with some coins
bitcoin_regtest.testcoin_faucet(address=wallet.getnewaddress())
# Realize that the wallet has funds:
wallet.update()
assert not specter.wallet_manager.working_folder is None
yield specter
try:
yield specter
finally:
# Deleting all Wallets (this will also purge them on core)
for user in specter.user_manager.users:
for wallet in list(user.wallet_manager.wallets.values()):
user.wallet_manager.delete_wallet(
wallet, bitcoin_datadir=bitcoin_regtest.datadir, chain="regtest"
)
@pytest.fixture
def app(specter_regtest_configured):
"""the Flask-App, but uninitialized"""
app = create_app()
app = create_app(config="cryptoadvance.specter.config.TestConfig")
app.app_context().push()
app.config["TESTING"] = True
app.testing = True

View file

@ -27,6 +27,7 @@ mock_config_dict = {
@patch("cryptoadvance.specter.cli.cli_server.create_app")
@patch("cryptoadvance.specter.cli.cli_server.init_app")
def test_server_host_and_port(init_app, create_app, caplog):
"""This test will fail if you have turned on live-logging in pytest.ini (log_cli = 1 )"""
caplog.set_level(logging.DEBUG)
mock_app = MagicMock()
mock_app.config = MagicMock()
@ -51,6 +52,7 @@ def test_server_host_and_port(init_app, create_app, caplog):
@patch("cryptoadvance.specter.cli.cli_server.create_app")
@patch("cryptoadvance.specter.cli.cli_server.init_app")
def test_server_host_and_port(init_app, create_app, caplog):
"""This test will fail if you have turned on live-logging in pytest.ini (log_cli = 1 )"""
caplog.set_level(logging.DEBUG)
mock_app = MagicMock()
mock_app.config = MagicMock()
@ -90,6 +92,7 @@ def test_server_host_and_port(init_app, create_app, caplog):
@patch("cryptoadvance.specter.cli.cli_server.create_app")
@patch("cryptoadvance.specter.cli.cli_server.init_app")
def test_server_debug(init_app, create_app, caplog):
"""This test will fail if you have turned on live-logging in pytest.ini (log_cli = 1 )"""
caplog.set_level(logging.DEBUG)
runner = CliRunner()
result = runner.invoke(server, ["--debug", "--no-filelog"])
@ -105,6 +108,7 @@ def test_server_debug(init_app, create_app, caplog):
@patch("cryptoadvance.specter.cli.cli_server.create_app")
@patch("cryptoadvance.specter.cli.cli_server.init_app")
def test_server_datafolder(init_app, create_app, caplog):
"""This test will fail if you have turned on live-logging in pytest.ini (log_cli = 1 )"""
caplog.set_level(logging.DEBUG)
mock_app = MagicMock()
mock_app.config = MagicMock()
@ -130,6 +134,7 @@ def test_server_datafolder(init_app, create_app, caplog):
@patch("cryptoadvance.specter.cli.cli_server.create_app")
@patch("cryptoadvance.specter.cli.cli_server.init_app")
def test_server_config(init_app, create_app, caplog):
"""This test will fail if you have turned on live-logging in pytest.ini (log_cli = 1 )"""
caplog.set_level(logging.DEBUG)
mock_app = MagicMock()
mock_app.config = MagicMock()

View file

@ -1,4 +1,5 @@
import os
import logging
from cryptoadvance.specter.devices.generic import GenericDevice
from cryptoadvance.specter.key import Key
from cryptoadvance.specter.managers.device_manager import DeviceManager
@ -105,7 +106,10 @@ def test_DeviceManager(empty_data_folder):
assert some_device.keys[1] == another_key
def test_device_wallets(bitcoin_regtest, devices_filled_data_folder, device_manager):
def test_device_wallets(
bitcoin_regtest, devices_filled_data_folder, device_manager, caplog
):
caplog.set_level(logging.DEBUG)
wm = WalletManager(
200100,
devices_filled_data_folder,

9
tests/test_liquid_rpc.py Normal file
View file

@ -0,0 +1,9 @@
from cryptoadvance.specter.liquid.rpc import LiquidRPC
def test_LiquidRpc(elements_elreg):
rpc = elements_elreg.get_rpc()
default_rpc = rpc.wallet("")
assert default_rpc.getbalance() >= 0
# This test is failing although the documentation says it should work like this:
# assert default_rpc.getbalance(assetLabel=None)["bitcoin"] == 0

View file

@ -24,34 +24,38 @@ logger = logging.getLogger(__name__)
@pytest.mark.slow
@pytest.mark.skip("interferes with conftest fixtures")
def test_node_running_bitcoin(caplog, docker, request):
# TODO: Refactor this to use conftest.instantiate_bitcoind_controller
# to reduce redundant code?
caplog.set_level(logging.INFO)
caplog.set_level(logging.DEBUG, logger="cryptoadvance.specter")
requested_version = request.config.getoption("--bitcoind-version")
if docker:
my_bitcoind = BitcoindDockerController(
rpcport=18456, docker_tag=requested_version
)
pass
else:
my_bitcoind = BitcoindPlainController(
bitcoind_path=find_node_executable("bitcoin"),
rpcport=18456, # Non-standardport to not interfer
)
try:
if docker:
my_bitcoind = BitcoindDockerController(
rpcport=18456, docker_tag=requested_version
)
pass
else:
my_bitcoind = BitcoindPlainController(
bitcoind_path=find_node_executable("bitcoin"),
rpcport=18456, # Non-standardport to not interfer
)
rpcconn = my_bitcoind.start_node(cleanup_at_exit=True, cleanup_hard=True)
requested_version = request.config.getoption("--bitcoind-version")
assert my_bitcoind.version() == requested_version
assert rpcconn.get_rpc() != None
assert rpcconn.get_rpc().ipaddress != None
bci = rpcconn.get_rpc().getblockchaininfo()
assert bci["blocks"] == 100
# you can use the testcoin_faucet:
random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r"
my_bitcoind.testcoin_faucet(random_address, amount=25)
my_bitcoind.stop_node()
rpcconn = my_bitcoind.start_node(cleanup_at_exit=True, cleanup_hard=True)
requested_version = request.config.getoption("--bitcoind-version")
assert my_bitcoind.version() == requested_version
assert rpcconn.get_rpc() != None
assert rpcconn.get_rpc().ipaddress != None
bci = rpcconn.get_rpc().getblockchaininfo()
assert bci["blocks"] == 100
# you can use the testcoin_faucet:
random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r"
my_bitcoind.testcoin_faucet(random_address, amount=25)
finally:
my_bitcoind.stop_node()
logger.info("Bitcoind for test_node_running_bitcoin stopped")
def test_fetch_wallet_addresses_for_mining(caplog, wallets_filled_data_folder):
@ -63,36 +67,39 @@ def test_fetch_wallet_addresses_for_mining(caplog, wallets_filled_data_folder):
@pytest.mark.slow
@pytest.mark.skip("interferes with conftest fixtures")
def test_node_running_elements(caplog, docker, request):
# TODO: Refactor this to use conftest.instantiate_bitcoind_controller
# to reduce redundant code?
caplog.set_level(logging.INFO)
caplog.set_level(logging.DEBUG, logger="cryptoadvance.specter")
requested_version = request.config.getoption("--elementsd-version")
if docker:
# The NodeController is not available on docker
return
else:
try:
my_elementsd = ElementsPlainController(
elementsd_path=find_node_executable(node_impl="elements"),
rpcport=18123, # Non-standardport to not interfer
)
except Exception as e:
if "Couldn't find executable elementsd" in str(e):
pytest.skip(str(e))
else:
raise e
try:
if docker:
# The NodeController is not available on docker
return
else:
try:
my_elementsd = ElementsPlainController(
elementsd_path=find_node_executable(node_impl="elements"),
rpcport=18123, # Non-standardport to not interfer
)
except Exception as e:
if "Couldn't find executable elementsd" in str(e):
pytest.skip(str(e))
else:
raise e
rpcconn = my_elementsd.start_node(cleanup_at_exit=True, cleanup_hard=True)
requested_version = request.config.getoption("--elementsd-version")
assert my_elementsd.version() == requested_version
assert rpcconn.get_rpc() != None
assert rpcconn.get_rpc().ipaddress != None
bci = rpcconn.get_rpc().getblockchaininfo()
# assert bci["blocks"] == 100
# you can use the testcoin_faucet:
prepare_elements_default_wallet(my_elementsd)
random_address = "el1qqf6tv4n8qp55qc04v4xts5snd9v5uurkry4vskef6lmecahj6c42jt9lnj0432287rs67z9vzq2zvuer036s5mahptwxgyd8k"
my_elementsd.testcoin_faucet(random_address, amount=25)
my_elementsd.stop_node()
rpcconn = my_elementsd.start_node(cleanup_at_exit=True, cleanup_hard=True)
requested_version = request.config.getoption("--elementsd-version")
assert my_elementsd.version() == requested_version
assert rpcconn.get_rpc() != None
assert rpcconn.get_rpc().ipaddress != None
bci = rpcconn.get_rpc().getblockchaininfo()
# assert bci["blocks"] == 100
# you can use the testcoin_faucet:
prepare_elements_default_wallet(my_elementsd)
random_address = "el1qqf6tv4n8qp55qc04v4xts5snd9v5uurkry4vskef6lmecahj6c42jt9lnj0432287rs67z9vzq2zvuer036s5mahptwxgyd8k"
my_elementsd.testcoin_faucet(random_address, amount=25)
finally:
my_elementsd.stop_node()

122
tests/test_rest.py Normal file
View file

@ -0,0 +1,122 @@
import pytest
import json
import base64
import logging
def test_rr_psbt_get(client, caplog):
caplog.set_level(logging.DEBUG)
""" testing the registration """
# Unauthorized
result = client.get("/api/v1alpha/wallets/some_wallet/psbt", follow_redirects=True)
assert result.status_code == 401
assert json.loads(result.data)["message"].startswith(
"The server could not verify that you are authorized to access the URL requested."
)
# Wrong password
headers = {
"Authorization": "Basic "
+ base64.b64encode(bytes("admin" + ":" + "wrongPassword", "ascii")).decode(
"ascii"
)
}
result = client.get(
"/api/v1alpha/wallets/simple/psbt", follow_redirects=True, headers=headers
)
assert result.status_code == 401
assert json.loads(result.data)["message"].startswith(
"The server could not verify that you are authorized to access the URL requested."
)
# Admin but not authorized (admin is NOT allowed to read everything)
headers = {
"Authorization": "Basic "
+ base64.b64encode(bytes("admin" + ":" + "admin", "ascii")).decode("ascii")
}
result = client.get(
"/api/v1alpha/wallets/simple/psbt", follow_redirects=True, headers=headers
)
assert result.status_code == 403
print(result.data)
assert json.loads(result.data)["message"].startswith("Wallet simple does not exist")
# Proper authorized (the wallet is owned by someuser)
headers = {
"Authorization": "Basic "
+ base64.b64encode(bytes("someuser" + ":" + "somepassword", "ascii")).decode(
"ascii"
)
}
result = client.get(
"/api/v1alpha/wallets/a_simple_wallet/psbt",
follow_redirects=True,
headers=headers,
)
assert result.status_code == 200
data = json.loads(result.data)
assert data["result"] == []
def test_rr_psbt_post(client, caplog):
caplog.set_level(logging.DEBUG)
""" testing the registration """
result = client.post(
"/api/v1alpha/wallets/some_wallet/psbt",
data=dict(address="someaddress", amount=0.5),
follow_redirects=True,
)
assert result.status_code == 401
assert json.loads(result.data)["message"].startswith(
"The server could not verify that you are authorized to access the URL requested."
)
headers = {
"Authorization": "Basic "
+ base64.b64encode(bytes("someuser" + ":" + "somepassword", "ascii")).decode(
"ascii"
),
"Content-type": "application/json",
}
result = client.post(
"/api/v1alpha/wallets/a_simple_wallet/psbt",
data="""
{
"recipients" : [
{
"address": "BCRT1qgc6h85z43g3ss2dl5zdrzrp3ef6av4neqcqhh8",
"amount": 0.1,
"unit": "btc",
"label": "someLabel"
},
{
"address": "bcrt1q3kfetuxpxvujasww6xas94nawklvpz0e52uw8a",
"amount": 111211,
"unit": "sat",
"label": "someOtherLabel"
}
],
"rbf_tx_id": "",
"subtract_from": "1",
"fee_rate": "64",
"rbf": true
}
""",
follow_redirects=True,
headers=headers,
)
print(result.data)
assert result.status_code == 200
data = json.loads(result.data)
assert "bcrt1qgc6h85z43g3ss2dl5zdrzrp3ef6av4neqcqhh8" in data["result"]["address"]
assert "bcrt1q3kfetuxpxvujasww6xas94nawklvpz0e52uw8a" in data["result"]["address"]
assert 0.1 in data["result"]["amount"]
assert 0.00111211 in data["result"]["amount"]
assert data["result"]["tx"]
assert data["result"]["inputs"]
assert data["result"]["outputs"]
assert data["result"]["fee_rate"] == "0.00064000"
assert data["result"]["tx_full_size"]
assert data["result"]["base64"]
assert data["result"]["time"]
assert data["result"]["sigs_count"] == 0

View file

@ -1,4 +1,4 @@
import json, logging, pytest
import json, logging, pytest, time, os
from decimal import Decimal
from cryptoadvance.specter.helpers import alias, generate_mnemonic
from cryptoadvance.specter.key import Key
@ -6,6 +6,7 @@ from cryptoadvance.specter.rpc import BitcoinRPC
from cryptoadvance.specter.specter import Specter
from cryptoadvance.specter.specter_error import SpecterError
from cryptoadvance.specter.managers.wallet_manager import WalletManager
from conftest import instantiate_bitcoind_controller
def test_alias():
@ -35,7 +36,6 @@ def test_abandon_purged_tx(
# from the mempool. Test starts a new bitcoind with a restricted mempool to make it
# easier to spam the mempool and purge our target tx.
# TODO: Similar test but for maxmempoolexpiry?
# Copied and adapted from:
# https://github.com/bitcoin/bitcoin/blob/master/test/functional/mempool_limit.py
from bitcoin_core.test.functional.test_framework.util import (
@ -43,7 +43,6 @@ def test_abandon_purged_tx(
satoshi_round,
create_lots_of_big_transactions,
)
from conftest import instantiate_bitcoind_controller
caplog.set_level(logging.DEBUG)
@ -53,150 +52,156 @@ def test_abandon_purged_tx(
bitcoind_controller = instantiate_bitcoind_controller(
docker,
request,
rpcport=18998,
rpcport=18968,
extra_args=["-acceptnonstdtxn=1", "-maxmempool=5", "-spendzeroconfchange=0"],
)
rpcconn = bitcoind_controller.rpcconn
rpc = rpcconn.get_rpc()
assert rpc is not None
assert rpc.ipaddress != None
# Note: Our utxo creation is simpler than mempool_limit.py's approach since we're
# running in regtest and can just use generatetoaddress().
# Instantiate a new Specter instance to talk to this bitcoind
config = {
"rpc": {
"autodetect": False,
"datadir": "",
"user": rpcconn.rpcuser,
"password": rpcconn.rpcpassword,
"port": rpcconn.rpcport,
"host": rpcconn.ipaddress,
"protocol": "http",
},
"auth": {
"method": "rpcpasswordaspin",
},
}
specter = Specter(data_folder=devices_filled_data_folder, config=config)
specter.check()
assert specter.info["mempool_info"]["maxmempool"] == 5 * 1000 * 1000 # 5MB
# Largely copy-and-paste from test_wallet_manager.test_wallet_createpsbt.
# TODO: Make a test fixture in conftest.py that sets up already funded wallets
# for a bitcoin core hot wallet.
wallet_manager = WalletManager(
200100,
devices_filled_data_folder,
rpc,
"regtest",
device_manager,
allow_threading=False,
)
# Create a new device that can sign psbts (Bitcoin Core hot wallet)
device = device_manager.add_device(
name="bitcoin_core_hot_wallet", device_type="bitcoincore", keys=[]
)
device.setup_device(file_password=None, wallet_manager=wallet_manager)
device.add_hot_wallet_keys(
mnemonic=generate_mnemonic(strength=128),
passphrase="",
paths=["m/49h/0h/0h"],
file_password=None,
wallet_manager=wallet_manager,
testnet=True,
keys_range=[0, 1000],
keys_purposes=[],
)
wallet = wallet_manager.create_wallet(
"bitcoincore_test_wallet", 1, "sh-wpkh", [device.keys[0]], [device]
)
# Fund the wallet. Going to need a LOT of utxos to play with.
logging.info("Generating utxos to wallet")
address = wallet.getnewaddress()
wallet.rpc.generatetoaddress(91, address)
# newly minted coins need 100 blocks to get spendable
# let's mine another 100 blocks to get these coins spendable
wallet.rpc.generatetoaddress(101, address)
# update the wallet data
wallet.get_balance()
# ==== Begin test from mempool_limit.py ====
txouts = gen_return_txouts()
relayfee = satoshi_round(rpc.getnetworkinfo()["relayfee"])
logging.info("Check that mempoolminfee is minrelytxfee")
assert satoshi_round(rpc.getmempoolinfo()["minrelaytxfee"]) == Decimal("0.00001000")
assert satoshi_round(rpc.getmempoolinfo()["mempoolminfee"]) == Decimal("0.00001000")
txids = []
utxos = wallet.rpc.listunspent()
logging.info("Create a mempool tx that will be evicted")
us0 = utxos.pop()
inputs = [{"txid": us0["txid"], "vout": us0["vout"]}]
outputs = {wallet.getnewaddress(): 0.0001}
tx = wallet.rpc.createrawtransaction(inputs, outputs)
wallet.rpc.settxfee(str(relayfee)) # specifically fund this tx with low fee
txF = wallet.rpc.fundrawtransaction(tx)
wallet.rpc.settxfee(0) # return to automatic fee selection
txFS = device.sign_raw_tx(txF["hex"], wallet)
txid = wallet.rpc.sendrawtransaction(txFS["hex"])
# ==== Specter-specific: can't abandon a valid pending tx ====
try:
wallet.abandontransaction(txid)
except SpecterError as e:
assert "Cannot abandon" in str(e)
assert bitcoind_controller.get_rpc().test_connection()
rpcconn = bitcoind_controller.rpcconn
rpc = rpcconn.get_rpc()
assert rpc is not None
assert rpc.ipaddress != None
# ==== Resume test from mempool_limit.py ====
# Spam the mempool with big transactions!
relayfee = satoshi_round(rpc.getnetworkinfo()["relayfee"])
base_fee = float(relayfee) * 100
for i in range(3):
txids.append([])
txids[i] = create_lots_of_big_transactions(
wallet, txouts, utxos[30 * i : 30 * i + 30], 30, (i + 1) * base_fee
# Note: Our utxo creation is simpler than mempool_limit.py's approach since we're
# running in regtest and can just use generatetoaddress().
# Instantiate a new Specter instance to talk to this bitcoind
config = {
"rpc": {
"autodetect": False,
"datadir": "",
"user": rpcconn.rpcuser,
"password": rpcconn.rpcpassword,
"port": rpcconn.rpcport,
"host": rpcconn.ipaddress,
"protocol": "http",
},
"auth": {
"method": "rpcpasswordaspin",
},
}
specter = Specter(data_folder=devices_filled_data_folder, config=config)
specter.check()
assert specter.info["mempool_info"]["maxmempool"] == 5 * 1000 * 1000 # 5MB
# Largely copy-and-paste from test_wallet_manager.test_wallet_createpsbt.
# TODO: Make a test fixture in conftest.py that sets up already funded wallets
# for a bitcoin core hot wallet.
wallet_manager = WalletManager(
200100,
devices_filled_data_folder,
rpc,
"regtest",
device_manager,
allow_threading=False,
)
logging.info("The tx should be evicted by now")
assert txid not in wallet.rpc.getrawmempool()
txdata = wallet.rpc.gettransaction(txid)
assert txdata["confirmations"] == 0 # confirmation should still be 0
# Create a new device that can sign psbts (Bitcoin Core hot wallet)
device = device_manager.add_device(
name="bitcoin_core_hot_wallet", device_type="bitcoincore", keys=[]
)
device.setup_device(file_password=None, wallet_manager=wallet_manager)
device.add_hot_wallet_keys(
mnemonic=generate_mnemonic(strength=128),
passphrase="",
paths=["m/49h/0h/0h"],
file_password=None,
wallet_manager=wallet_manager,
testnet=True,
keys_range=[0, 1000],
keys_purposes=[],
)
# ==== Specter-specific: Verify purge and abandon ====
assert wallet.is_tx_purged(txid)
wallet.abandontransaction(txid)
wallet = wallet_manager.create_wallet(
"bitcoincore_test_wallet", 1, "sh-wpkh", [device.keys[0]], [device]
)
# tx will still be in the wallet but marked "abandoned"
txdata = wallet.rpc.gettransaction(txid)
for detail in txdata["details"]:
if detail["category"] == "send":
assert detail["abandoned"]
# Fund the wallet. Going to need a LOT of utxos to play with.
logging.info("Generating utxos to wallet")
address = wallet.getnewaddress()
wallet.rpc.generatetoaddress(91, address)
# Can we now spend those same inputs?
outputs = {wallet.getnewaddress(): 0.0001}
tx = wallet.rpc.createrawtransaction(inputs, outputs)
# newly minted coins need 100 blocks to get spendable
# let's mine another 100 blocks to get these coins spendable
wallet.rpc.generatetoaddress(101, address)
# Fund this tx with a high enough fee
relayfee = satoshi_round(rpc.getnetworkinfo()["relayfee"])
wallet.rpc.settxfee(str(relayfee * Decimal("3.0")))
# update the wallet data
wallet.get_balance()
txF = wallet.rpc.fundrawtransaction(tx)
wallet.rpc.settxfee(0) # return to automatic fee selection
txFS = device.sign_raw_tx(txF["hex"], wallet)
txid = wallet.rpc.sendrawtransaction(txFS["hex"])
# ==== Begin test from mempool_limit.py ====
txouts = gen_return_txouts()
relayfee = satoshi_round(rpc.getnetworkinfo()["relayfee"])
# Should have been accepted by the mempool
assert txid in wallet.rpc.getrawmempool()
assert wallet.get_balance()["untrusted_pending"] == 0.0001
logging.info("Check that mempoolminfee is minrelytxfee")
assert satoshi_round(rpc.getmempoolinfo()["minrelaytxfee"]) == Decimal(
"0.00001000"
)
assert satoshi_round(rpc.getmempoolinfo()["mempoolminfee"]) == Decimal(
"0.00001000"
)
# Clean up
bitcoind_controller.stop_bitcoind()
txids = []
utxos = wallet.rpc.listunspent()
logging.info("Create a mempool tx that will be evicted")
us0 = utxos.pop()
inputs = [{"txid": us0["txid"], "vout": us0["vout"]}]
outputs = {wallet.getnewaddress(): 0.0001}
tx = wallet.rpc.createrawtransaction(inputs, outputs)
wallet.rpc.settxfee(str(relayfee)) # specifically fund this tx with low fee
txF = wallet.rpc.fundrawtransaction(tx)
wallet.rpc.settxfee(0) # return to automatic fee selection
txFS = device.sign_raw_tx(txF["hex"], wallet)
txid = wallet.rpc.sendrawtransaction(txFS["hex"])
# ==== Specter-specific: can't abandon a valid pending tx ====
try:
wallet.abandontransaction(txid)
except SpecterError as e:
assert "Cannot abandon" in str(e)
# ==== Resume test from mempool_limit.py ====
# Spam the mempool with big transactions!
relayfee = satoshi_round(rpc.getnetworkinfo()["relayfee"])
base_fee = float(relayfee) * 100
for i in range(3):
txids.append([])
txids[i] = create_lots_of_big_transactions(
wallet, txouts, utxos[30 * i : 30 * i + 30], 30, (i + 1) * base_fee
)
logging.info("The tx should be evicted by now")
assert txid not in wallet.rpc.getrawmempool()
txdata = wallet.rpc.gettransaction(txid)
assert txdata["confirmations"] == 0 # confirmation should still be 0
# ==== Specter-specific: Verify purge and abandon ====
assert wallet.is_tx_purged(txid)
wallet.abandontransaction(txid)
# tx will still be in the wallet but marked "abandoned"
txdata = wallet.rpc.gettransaction(txid)
for detail in txdata["details"]:
if detail["category"] == "send":
assert detail["abandoned"]
# Can we now spend those same inputs?
outputs = {wallet.getnewaddress(): 0.0001}
tx = wallet.rpc.createrawtransaction(inputs, outputs)
# Fund this tx with a high enough fee
relayfee = satoshi_round(rpc.getnetworkinfo()["relayfee"])
wallet.rpc.settxfee(str(relayfee * Decimal("3.0")))
txF = wallet.rpc.fundrawtransaction(tx)
wallet.rpc.settxfee(0) # return to automatic fee selection
txFS = device.sign_raw_tx(txF["hex"], wallet)
txid = wallet.rpc.sendrawtransaction(txFS["hex"])
# Should have been accepted by the mempool
assert txid in wallet.rpc.getrawmempool()
assert wallet.get_balance()["untrusted_pending"] == 0.0001
finally:
# Clean up
bitcoind_controller.stop_bitcoind()

View file

@ -5,7 +5,7 @@ from cryptoadvance.specter.util.psbt_creator import PsbtCreator
from mock import MagicMock, call, patch
def test_PsbtCreator(caplog):
def test_PsbtCreator_ui(caplog):
caplog.set_level(logging.DEBUG)
specter_mock = MagicMock()
# non liquid and default asset btc (for unit-calculation)
@ -58,3 +58,115 @@ def test_PsbtCreator(caplog):
}
psbt_creator.create_psbt(wallet_mock)
def test_PsbtCreator_text(caplog):
caplog.set_level(logging.DEBUG)
specter_mock = MagicMock()
# non liquid and default asset btc (for unit-calculation)
specter_mock.is_liquid = False
specter_mock.default_asset = "btc"
wallet_mock = MagicMock()
specter_mock.chain = "regtest"
# Let's mock the request.form which behaves like a dict but also needs getlist()
request_form_data = {
"rbf_tx_id": "",
"subtract_from": "1",
"fee_options": "dynamic",
"fee_rate": "",
"fee_rate_dynamic": "64",
"rbf": "on",
"action": "createpsbt",
}
recipients_txt = """
BCRT1qgc6h85z43g3ss2dl5zdrzrp3ef6av4neqcqhh8, 0.1
bcrt1q3kfetuxpxvujasww6xas94nawklvpz0e52uw8a, 0.5
"""
psbt_creator: PsbtCreator = PsbtCreator(
specter_mock,
wallet_mock,
"text",
request_form=request_form_data,
recipients_txt=recipients_txt,
recipients_amount_unit="btc",
)
assert psbt_creator.addresses == [
"bcrt1qgc6h85z43g3ss2dl5zdrzrp3ef6av4neqcqhh8",
"bcrt1q3kfetuxpxvujasww6xas94nawklvpz0e52uw8a",
]
assert psbt_creator.amounts == [0.1, 0.5]
# no labeling for the text-option
# assert psbt_creator.labels == ["someLabel", "someOtherLabel"]
assert psbt_creator.amount_units == ["btc", "btc"]
assert psbt_creator.kwargs == {
"fee_rate": 64.0,
"rbf": True,
"rbf_edit_mode": False,
"readonly": False,
"selected_coins": None,
"subtract": False,
"subtract_from": 0,
}
psbt_creator.create_psbt(wallet_mock)
def test_PsbtCreator_json(caplog):
caplog.set_level(logging.DEBUG)
specter_mock = MagicMock()
# non liquid and default asset btc (for unit-calculation)
specter_mock.is_liquid = False
specter_mock.default_asset = "btc"
wallet_mock = MagicMock()
specter_mock.chain = "regtest"
# Let's mock the request.form which behaves like a dict but also needs getlist()
request_json = """
{
"recipients" : [
{
"address": "BCRT1qgc6h85z43g3ss2dl5zdrzrp3ef6av4neqcqhh8",
"amount": 0.1,
"unit": "btc",
"label": "someLabel"
},
{
"address": "bcrt1q3kfetuxpxvujasww6xas94nawklvpz0e52uw8a",
"amount": 111211,
"unit": "sat",
"label": "someOtherLabel"
}
],
"rbf_tx_id": "",
"subtract_from": "1",
"fee_rate": "64",
"rbf": true
}
"""
psbt_creator: PsbtCreator = PsbtCreator(
specter_mock, wallet_mock, "json", request_json=request_json
)
assert psbt_creator.addresses == [
"bcrt1qgc6h85z43g3ss2dl5zdrzrp3ef6av4neqcqhh8",
"bcrt1q3kfetuxpxvujasww6xas94nawklvpz0e52uw8a",
]
assert psbt_creator.amounts == [0.1, 0.00111211]
assert psbt_creator.labels == ["someLabel", "someOtherLabel"]
assert psbt_creator.amount_units == ["btc", "sat"]
assert psbt_creator.kwargs == {
"fee_rate": 64.0,
"rbf": True,
"rbf_edit_mode": False,
"readonly": False,
"selected_coins": [],
"subtract": False,
"subtract_from": 0,
}
psbt_creator.create_psbt(wallet_mock)

View file

@ -1,11 +1,15 @@
import logging
import time
from cryptoadvance.specter.key import Key
from cryptoadvance.specter.managers.device_manager import DeviceManager
from cryptoadvance.specter.user import User
from cryptoadvance.specter.util.descriptor import Descriptor
from cryptoadvance.specter.util.wallet_importer import WalletImporter
from mock import MagicMock, call, patch
def test_WalletImporter():
def test_WalletImporter_unit():
specter_mock = MagicMock()
specter_mock.chain = "regtest"
wallet_json = """
@ -66,3 +70,39 @@ def test_WalletImporter():
assert wm_mock.create_wallet.called
wm_mock.create_wallet.assert_called_once
assert wallet_mock.keypoolrefill.called
def test_WalletImporter_integration(specter_regtest_configured, bitcoin_regtest):
specter = specter_regtest_configured
someuser: User = specter.user_manager.add_user(
User.from_json(
{
"id": "someuser",
"username": "someuser",
"password": "somepassword",
"config": {},
"is_admin": False,
},
specter,
)
)
specter.user_manager.save()
specter.check()
# Create a Wallet
wallet_json = '{"label": "another_simple_wallet", "blockheight": 0, "descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/0/*)#xp8lv5nr", "devices": [{"type": "trezor", "label": "trezor"}]} '
wallet_importer = WalletImporter(
wallet_json, specter, device_manager=someuser.device_manager
)
wallet_importer.create_nonexisting_signers(
someuser.device_manager,
{"unknown_cosigner_0_name": "trezor", "unknown_cosigner_0_type": "trezor"},
)
dm: DeviceManager = someuser.device_manager
wallet = wallet_importer.create_wallet(someuser.wallet_manager)
# fund it with some coins
bitcoin_regtest.testcoin_faucet(address=wallet.getnewaddress())
# Realize that the wallet has funds:
wallet.update()
wallet = someuser.wallet_manager.get_by_alias("another_simple_wallet")
assert wallet.get_balance()["untrusted_pending"] == 20

View file

@ -25,173 +25,174 @@ def test_WalletManager(docker, request, devices_filled_data_folder, device_manag
bitcoind_controller = instantiate_bitcoind_controller(
docker, request, rpcport=18998
)
try:
wm = WalletManager(
200100,
devices_filled_data_folder,
bitcoind_controller.get_rpc(),
"regtest",
device_manager,
allow_threading=False,
)
# A wallet-creation needs a device
device = device_manager.get_by_alias("trezor")
assert device != None
# Lets's create a wallet with the WalletManager
wm.create_wallet("a_test_wallet", 1, "wpkh", [device.keys[5]], [device])
# The wallet-name gets its filename and therefore its alias
wallet = wm.wallets["a_test_wallet"]
assert wallet != None
assert wallet.balance["trusted"] == 0
assert wallet.balance["untrusted_pending"] == 0
# this is a sum of both
assert wallet.fullbalance == 0
address = wallet.getnewaddress()
# newly minted coins need 100 blocks to get spendable
wallet.rpc.generatetoaddress(1, address)
# let's mine another 100 blocks to get these coins spendable
random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r"
wallet.rpc.generatetoaddress(100, random_address)
# update the balance
wallet.get_balance()
assert wallet.fullbalance >= 25
wm = WalletManager(
200100,
devices_filled_data_folder,
bitcoind_controller.rpcconn.get_rpc(),
"regtest",
device_manager,
allow_threading=False,
)
# A wallet-creation needs a device
device = device_manager.get_by_alias("trezor")
assert device != None
# Lets's create a wallet with the WalletManager
wm.create_wallet("a_test_wallet", 1, "wpkh", [device.keys[5]], [device])
# The wallet-name gets its filename and therefore its alias
wallet = wm.wallets["a_test_wallet"]
assert wallet != None
assert wallet.balance["trusted"] == 0
assert wallet.balance["untrusted_pending"] == 0
# this is a sum of both
assert wallet.fullbalance == 0
address = wallet.getnewaddress()
# newly minted coins need 100 blocks to get spendable
wallet.rpc.generatetoaddress(1, address)
# let's mine another 100 blocks to get these coins spendable
random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r"
wallet.rpc.generatetoaddress(100, random_address)
# update the balance
wallet.get_balance()
assert wallet.fullbalance >= 25
# You can create a multisig wallet with the wallet manager like this
second_device = device_manager.get_by_alias("specter")
multisig_wallet = wm.create_wallet(
"a_multisig_test_wallet",
1,
"wsh",
[device.keys[7], second_device.keys[0]],
[device, second_device],
)
# You can create a multisig wallet with the wallet manager like this
second_device = device_manager.get_by_alias("specter")
multisig_wallet = wm.create_wallet(
"a_multisig_test_wallet",
1,
"wsh",
[device.keys[7], second_device.keys[0]],
[device, second_device],
)
assert len(wm.wallets) == 2
assert multisig_wallet != None
assert multisig_wallet.fullbalance == 0
multisig_address = multisig_wallet.getnewaddress()
multisig_wallet.rpc.generatetoaddress(1, multisig_address)
multisig_wallet.rpc.generatetoaddress(100, random_address)
# update balance
multisig_wallet.get_balance()
assert multisig_wallet.fullbalance >= 12.5
# The WalletManager also has a `wallets_names` property, returning a sorted list of the names of all wallets
assert wm.wallets_names == ["a_multisig_test_wallet", "a_test_wallet"]
assert len(wm.wallets) == 2
assert multisig_wallet != None
assert multisig_wallet.fullbalance == 0
multisig_address = multisig_wallet.getnewaddress()
multisig_wallet.rpc.generatetoaddress(1, multisig_address)
multisig_wallet.rpc.generatetoaddress(100, random_address)
# update balance
multisig_wallet.get_balance()
assert multisig_wallet.fullbalance >= 12.5
# The WalletManager also has a `wallets_names` property, returning a sorted list of the names of all wallets
assert wm.wallets_names == ["a_multisig_test_wallet", "a_test_wallet"]
# You can rename a wallet using the wallet manager using `rename_wallet`, passing the wallet object and the new name to assign to it
wm.rename_wallet(multisig_wallet, "new_name_test_wallet")
assert multisig_wallet.name == "new_name_test_wallet"
assert wm.wallets_names == ["a_test_wallet", "new_name_test_wallet"]
# You can rename a wallet using the wallet manager using `rename_wallet`, passing the wallet object and the new name to assign to it
wm.rename_wallet(multisig_wallet, "new_name_test_wallet")
assert multisig_wallet.name == "new_name_test_wallet"
assert wm.wallets_names == ["a_test_wallet", "new_name_test_wallet"]
# you can also delete a wallet by passing it to the wallet manager's `delete_wallet` method
# it will delete the json and attempt to remove it from Bitcoin Core
wallet_fullpath = multisig_wallet.fullpath
assert os.path.exists(wallet_fullpath)
wm.delete_wallet(multisig_wallet)
assert not os.path.exists(wallet_fullpath)
assert len(wm.wallets) == 1
# cleanup
bitcoind_controller.stop_bitcoind()
# you can also delete a wallet by passing it to the wallet manager's `delete_wallet` method
# it will delete the json and attempt to remove it from Bitcoin Core
wallet_fullpath = multisig_wallet.fullpath
assert os.path.exists(wallet_fullpath)
wm.delete_wallet(multisig_wallet)
assert not os.path.exists(wallet_fullpath)
assert len(wm.wallets) == 1
finally:
# cleanup
bitcoind_controller.stop_bitcoind()
@pytest.mark.slow
def test_wallet_createpsbt(docker, request, devices_filled_data_folder, device_manager):
# Instantiate a fresh bitcoind instance to isolate this test.
bitcoind_controller = instantiate_bitcoind_controller(
docker, request, rpcport=18998
docker, request, rpcport=18978
)
wm = WalletManager(
200100,
devices_filled_data_folder,
bitcoind_controller.rpcconn.get_rpc(),
"regtest",
device_manager,
allow_threading=False,
)
# A wallet-creation needs a device
device = device_manager.get_by_alias("specter")
key = Key.from_json(
{
"derivation": "m/48h/1h/0h/2h",
"original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM",
"fingerprint": "08686ac6",
"type": "wsh",
"xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL",
}
)
wallet = wm.create_wallet("a_second_test_wallet", 1, "wpkh", [key], [device])
# Let's fund the wallet with ... let's say 40 blocks a 50 coins each --> 200 coins
address = wallet.getnewaddress()
assert address == "bcrt1qtnrv2jpygx2ef3zqfjhqplnycxak2m6ljnhq6z"
wallet.rpc.generatetoaddress(20, address)
# in two addresses
address = wallet.getnewaddress()
wallet.rpc.generatetoaddress(20, address)
# newly minted coins need 100 blocks to get spendable
# let's mine another 100 blocks to get these coins spendable
random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r"
wallet.rpc.generatetoaddress(110, random_address)
# update the wallet data
wallet.get_balance()
# Now we have loads of potential inputs
# Let's spend 500 coins
assert wallet.fullbalance >= 250
# From this print-statement, let's grab some txids which we'll use for coinselect
unspents = wallet.rpc.listunspent(0)
# Lets take 3 more or less random txs from the unspents:
selected_coins = [
"{},{}".format(unspents[5]["txid"], unspents[5]["vout"]),
"{},{}".format(unspents[9]["txid"], unspents[9]["vout"]),
"{},{}".format(unspents[12]["txid"], unspents[12]["vout"]),
]
selected_coins_amount_sum = (
unspents[5]["amount"] + unspents[9]["amount"] + unspents[12]["amount"]
)
number_of_coins_to_spend = (
selected_coins_amount_sum - 0.1
) # Let's spend almost all of them
psbt = wallet.createpsbt(
[random_address],
[number_of_coins_to_spend],
True,
0,
10,
selected_coins=selected_coins,
)
assert len(psbt["tx"]["vin"]) == 3
psbt_txs = [tx["txid"] for tx in psbt["tx"]["vin"]]
for coin in selected_coins:
assert coin.split(",")[0] in psbt_txs
# Now let's spend more coins than we have selected. This should result in an exception:
try:
wm = WalletManager(
200100,
devices_filled_data_folder,
bitcoind_controller.rpcconn.get_rpc(),
"regtest",
device_manager,
allow_threading=False,
)
# A wallet-creation needs a device
device = device_manager.get_by_alias("specter")
key = Key.from_json(
{
"derivation": "m/48h/1h/0h/2h",
"original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM",
"fingerprint": "08686ac6",
"type": "wsh",
"xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL",
}
)
wallet = wm.create_wallet("a_second_test_wallet", 1, "wpkh", [key], [device])
# Let's fund the wallet with ... let's say 40 blocks a 50 coins each --> 200 coins
address = wallet.getnewaddress()
assert address == "bcrt1qtnrv2jpygx2ef3zqfjhqplnycxak2m6ljnhq6z"
wallet.rpc.generatetoaddress(20, address)
# in two addresses
address = wallet.getnewaddress()
wallet.rpc.generatetoaddress(20, address)
# newly minted coins need 100 blocks to get spendable
# let's mine another 100 blocks to get these coins spendable
random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r"
wallet.rpc.generatetoaddress(110, random_address)
# update the wallet data
wallet.get_balance()
# Now we have loads of potential inputs
# Let's spend 500 coins
assert wallet.fullbalance >= 250
# From this print-statement, let's grab some txids which we'll use for coinselect
unspents = wallet.rpc.listunspent(0)
# Lets take 3 more or less random txs from the unspents:
selected_coins = [
"{},{}".format(unspents[5]["txid"], unspents[5]["vout"]),
"{},{}".format(unspents[9]["txid"], unspents[9]["vout"]),
"{},{}".format(unspents[12]["txid"], unspents[12]["vout"]),
]
selected_coins_amount_sum = (
unspents[5]["amount"] + unspents[9]["amount"] + unspents[12]["amount"]
)
number_of_coins_to_spend = (
selected_coins_amount_sum - 0.1
) # Let's spend almost all of them
psbt = wallet.createpsbt(
[random_address],
[number_of_coins_to_spend + 1],
[number_of_coins_to_spend],
True,
0,
10,
selected_coins=selected_coins,
)
assert False, "should throw an exception!"
except SpecterError as e:
pass
assert len(psbt["tx"]["vin"]) == 3
psbt_txs = [tx["txid"] for tx in psbt["tx"]["vin"]]
for coin in selected_coins:
assert coin.split(",")[0] in psbt_txs
assert wallet.locked_amount == selected_coins_amount_sum
assert len(wallet.rpc.listlockunspent()) == 3
assert (
wallet.full_available_balance == wallet.fullbalance - selected_coins_amount_sum
)
# Now let's spend more coins than we have selected. This should result in an exception:
try:
psbt = wallet.createpsbt(
[random_address],
[number_of_coins_to_spend + 1],
True,
0,
10,
selected_coins=selected_coins,
)
assert False, "should throw an exception!"
except SpecterError as e:
pass
wallet.delete_pending_psbt(psbt["tx"]["txid"])
assert wallet.locked_amount == 0
assert len(wallet.rpc.listlockunspent()) == 0
assert wallet.full_available_balance == wallet.fullbalance
assert wallet.locked_amount == selected_coins_amount_sum
assert len(wallet.rpc.listlockunspent()) == 3
assert (
wallet.full_available_balance
== wallet.fullbalance - selected_coins_amount_sum
)
# cleanup
bitcoind_controller.stop_bitcoind()
wallet.delete_pending_psbt(psbt["tx"]["txid"])
assert wallet.locked_amount == 0
assert len(wallet.rpc.listlockunspent()) == 0
assert wallet.full_available_balance == wallet.fullbalance
finally:
# cleanup
bitcoind_controller.stop_bitcoind()
def test_wallet_sortedmulti(