mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
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:
parent
92b842525b
commit
e10cea0456
49 changed files with 2614 additions and 693 deletions
|
|
@ -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
11
.dockerignore
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
.env
|
||||
pyinstaller
|
||||
.git
|
||||
release
|
||||
docs
|
||||
.vscode
|
||||
tests
|
||||
docker
|
||||
.pytest_cache
|
||||
build
|
||||
dist
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -31,5 +31,4 @@ elmd-conn.json
|
|||
tests/bitcoin
|
||||
tests/bitcoin.binary
|
||||
tests/bitcoin.compile
|
||||
bla
|
||||
blub
|
||||
token.sh
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
62
docker/cypress-base-ubuntu-focal/Dockerfile
Normal file
62
docker/cypress-base-ubuntu-focal/Dockerfile
Normal 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
|
||||
11
docker/cypress-base-ubuntu-focal/README.md
Normal file
11
docker/cypress-base-ubuntu-focal/README.md
Normal 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
|
||||
|
||||
|
|
@ -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
36
docs/api/README.md
Normal 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
24
docs/api/ep_liveness.md
Normal 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
26
docs/api/ep_readyness.md
Normal 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
198
docs/api/ep_specter.md
Normal 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"
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
46
docs/api/ep_specter_fulltxlist.md
Normal file
46
docs/api/ep_specter_fulltxlist.md
Normal 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
214
docs/api/ep_wallets_psbt.md
Normal 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.
|
||||
167
docs/api/ep_wallets_wallet.md
Normal file
167
docs/api/ep_wallets_wallet.md
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
|
|||
439
requirements.txt
439
requirements.txt
|
|
@ -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
|
||||
|
|
|
|||
16
src/cryptoadvance/specter/api/__init__.py
Normal file
16
src/cryptoadvance/specter/api/__init__.py
Normal 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
|
||||
0
src/cryptoadvance/specter/api/rest/__init__.py
Normal file
0
src/cryptoadvance/specter/api/rest/__init__.py
Normal file
82
src/cryptoadvance/specter/api/rest/api.py
Normal file
82
src/cryptoadvance/specter/api/rest/api.py
Normal 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))
|
||||
86
src/cryptoadvance/specter/api/rest/base.py
Normal file
86
src/cryptoadvance/specter/api/rest/base.py
Normal 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)
|
||||
42
src/cryptoadvance/specter/api/rest/resource_healthz.py
Normal file
42
src/cryptoadvance/specter/api/rest/resource_healthz.py
Normal 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"}
|
||||
41
src/cryptoadvance/specter/api/rest/resource_psbt.py
Normal file
41
src/cryptoadvance/specter/api/rest/resource_psbt.py
Normal 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}
|
||||
46
src/cryptoadvance/specter/api/rest/resource_specter.py
Normal file
46
src/cryptoadvance/specter/api/rest/resource_specter.py
Normal 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
|
||||
34
src/cryptoadvance/specter/api/rest/resource_txlist.py
Normal file
34
src/cryptoadvance/specter/api/rest/resource_txlist.py
Normal 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
|
||||
52
src/cryptoadvance/specter/api/security.py
Normal file
52
src/cryptoadvance/specter/api/security.py
Normal 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
|
||||
18
src/cryptoadvance/specter/api/templates/api.html
Normal file
18
src/cryptoadvance/specter/api/templates/api.html
Normal 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>
|
||||
11
src/cryptoadvance/specter/api/views.py
Normal file
11
src/cryptoadvance/specter/api/views.py
Normal 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()
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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" """
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]:
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@
|
|||
}
|
||||
showError(jsonResponse.error, 4000);
|
||||
} catch(e) {
|
||||
showError('{{ _("Failed to start Bitcoin Core...") }}';
|
||||
showError('{{ _("Failed to start Bitcoin Core...") }}');
|
||||
showError(e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}>"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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}>"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
9
tests/test_liquid_rpc.py
Normal 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
|
||||
|
|
@ -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
122
tests/test_rest.py
Normal 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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue