Compare commits
64 Commits
benma-bitb
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f988d4fffc | ||
|
|
d86368ff7d | ||
|
|
def565b5ac | ||
|
|
1b65d782e2 | ||
|
|
b45607c1cf | ||
|
|
070d5eafbf | ||
|
|
087bc691f2 | ||
|
|
b8f0901752 | ||
|
|
663169d754 | ||
|
|
afec2cd8fc | ||
|
|
7dbcc9ad79 | ||
|
|
085817746b | ||
|
|
f83c966012 | ||
|
|
0683dc246a | ||
|
|
17b8c8bf3c | ||
|
|
b24f2aaa2d | ||
|
|
2f56cd3839 | ||
|
|
5010844a27 | ||
|
|
6169788aa7 | ||
|
|
d255229639 | ||
|
|
4df060d0c6 | ||
|
|
ed1125115f | ||
|
|
4e4455d2b2 | ||
|
|
1d0bbb6281 | ||
|
|
db672d048b | ||
|
|
44dabcfa80 | ||
|
|
550161b937 | ||
|
|
2c381fa2dc | ||
|
|
2ea932ed78 | ||
|
|
4791841d63 | ||
|
|
22f6d7d62e | ||
|
|
991a14473d | ||
|
|
c5a64885bc | ||
|
|
f0cef6ea39 | ||
|
|
10f094a0b0 | ||
|
|
84eacd63e1 | ||
|
|
c6038cbbd9 | ||
|
|
e8316657a5 | ||
|
|
b9931556b1 | ||
|
|
66182f0ca7 | ||
|
|
4498c3f347 | ||
|
|
79f74a78fc | ||
|
|
8751d1c725 | ||
|
|
5e296ca4dc | ||
|
|
f7776fcfb3 | ||
|
|
3adaa3c483 | ||
|
|
7a896e792b | ||
|
|
287bce9894 | ||
|
|
914ad3ea66 | ||
|
|
78a8801f94 | ||
|
|
2fd8012be0 | ||
|
|
42ad6f8b95 | ||
|
|
85958afcc6 | ||
|
|
3016d4a861 | ||
|
|
85e5723289 | ||
|
|
56f38aae8d | ||
|
|
d16bdf794f | ||
|
|
c663a21ec8 | ||
|
|
488e9f2876 | ||
|
|
67a9d7a322 | ||
|
|
d427c22235 | ||
|
|
4be273bf45 | ||
|
|
a78f8d68f6 | ||
|
|
dfb3117053 |
11
.gitignore
vendored
11
.gitignore
vendored
@ -14,3 +14,14 @@ hwilib/ui/ui_*.py
|
||||
|
||||
*.stderr
|
||||
*.stdout
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
.vscode
|
||||
|
||||
@ -68,7 +68,9 @@ jobs:
|
||||
stage: lint
|
||||
install:
|
||||
- pip install mypy
|
||||
script: mypy --strict hwilib/base58.py hwilib/errors.py hwilib/serializations.py hwilib/hwwclient.py
|
||||
- pip install poetry
|
||||
- poetry install
|
||||
script: mypy --implicit-reexport --strict hwilib/base58.py hwilib/errors.py hwilib/serializations.py hwilib/hwwclient.py hwilib/devices/bitbox02.py
|
||||
- name: Run non-device tests only
|
||||
stage: test
|
||||
install:
|
||||
|
||||
16
HWI.entitlements
Normal file
16
HWI.entitlements
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-executable-page-protection</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
48
README.md
48
README.md
@ -6,6 +6,8 @@ The Bitcoin Hardware Wallet Interface is a Python library and command line tool
|
||||
It provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.
|
||||
Python software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.
|
||||
|
||||
Caveat emptor: Inclusion of a specific hardware wallet vendor does not imply any endorsement of quality or security.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Python 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed
|
||||
@ -88,29 +90,29 @@ The below table lists what devices and features are supported for each device.
|
||||
|
||||
Please also see [docs](docs/) for additional information about each device.
|
||||
|
||||
| Feature \ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | Digital BitBox | KeepKey | Coldcard |
|
||||
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Message Signing | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | N/A |
|
||||
| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | N/A |
|
||||
| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | N/A |
|
||||
| Device Backup | N/A | N/A | N/A | N/A | Yes | N/A | Yes |
|
||||
| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |
|
||||
| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |
|
||||
| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |
|
||||
| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |
|
||||
| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | N/A | Yes | Yes | Yes |
|
||||
| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes |
|
||||
| Feature \ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | BitBox01 | BitBox02 | KeepKey | Coldcard |
|
||||
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Message Signing | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |
|
||||
| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A |
|
||||
| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A |
|
||||
| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | Yes | N/A |
|
||||
| Device Backup | N/A | N/A | N/A | N/A | Yes | Yes | N/A | Yes |
|
||||
| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |
|
||||
| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |
|
||||
| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |
|
||||
| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |
|
||||
| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |
|
||||
| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |
|
||||
| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |
|
||||
| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |
|
||||
| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes | Yes |
|
||||
|
||||
## Using with Bitcoin Core
|
||||
|
||||
|
||||
49
docs/bitbox02.md
Normal file
49
docs/bitbox02.md
Normal file
@ -0,0 +1,49 @@
|
||||
# BitBox02
|
||||
|
||||
The BitBox02 is supported by HWI.
|
||||
|
||||
Current implemented commands are:
|
||||
|
||||
* `signtx`
|
||||
* `getxpub`
|
||||
* `displayaddress`
|
||||
* `setup`
|
||||
* `wipe`
|
||||
* `restore`
|
||||
* `backup`
|
||||
* `togglepassphrase`
|
||||
|
||||
Multisig (P2WSH only) is supported by the BitBox02, but is not ingerated into HWI yet. Coming
|
||||
soon^{tm}.
|
||||
|
||||
# Usage Notes
|
||||
|
||||
## Strict keypaths
|
||||
|
||||
The BitBox02 has strict keypath validation.
|
||||
|
||||
The only accepted keypaths for xpubs are:
|
||||
|
||||
- `m/49'/0'/<account'>` for `p2wpkh-p2sh` (segwit wrapped in P2SH)
|
||||
- `m/84'/0'/<account'>` for `p2wpkh` (native segwit v0)
|
||||
- `m/48'/0'/<account'>/2` for p2wsh multisig (native segwit v0 multisig).
|
||||
|
||||
`account'` can be between `0'` and `99'`.
|
||||
|
||||
For address keypaths, append `/0/<address index>` for a receive and `/1/<change index>` for a change
|
||||
address. Up to `10000` addresses are supported.
|
||||
|
||||
In `--testnet` mode, the second element must be `1'` (e.g. `m/49'/1'/...`).
|
||||
|
||||
## Signing with mixed input types
|
||||
|
||||
The BitBox02 allows mixing inputs of different script types (e.g. and `p2wpkh-p2sh` `p2wpkh`), as
|
||||
long as the keypaths use the appropriate bip44 purpose field per input (e.g. `49'` and `84'`) and
|
||||
all account indexes are the same.
|
||||
|
||||
Multisig and singlesig inputs cannot be mixed.
|
||||
|
||||
## getmasterxpub and legacy addresses not supported
|
||||
|
||||
`getmasterxpub` is the same as `getxpub` at the legacy keypath `m/44'/0'/0'`. Legacy xpub, addresses
|
||||
and inputs are not supported.
|
||||
@ -21,7 +21,6 @@ Due to the limitations of the Trezor, some transactions cannot be signed by a Tr
|
||||
- Multisig inputs are limited to at most n-of-15 multisigs. This is a firmware limitation.
|
||||
* Transactions with arbitrary input scripts (scriptPubKey, redeemScript, or witnessScript) and arbitrary output scripts cannot be signed. This is a firmware limitation.
|
||||
* Send-to-self transactions will result in no prompt for outputs as all outputs will be detected as change.
|
||||
- For **Trezor T**, a transaction cannot contain both segwit and non-segwit inputs
|
||||
|
||||
## Note on `backup`
|
||||
|
||||
|
||||
15
hwi.spec
15
hwi.spec
@ -33,14 +33,21 @@ pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='hwi',
|
||||
exclude_binaries=True,
|
||||
name='hwi',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
runtime_tmpdir=None,
|
||||
console=True )
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='hwi')
|
||||
|
||||
|
||||
@ -1 +1 @@
|
||||
__version__ = '1.1.2'
|
||||
__version__ = '1.2.1'
|
||||
|
||||
@ -9,6 +9,7 @@ from .serializations import PSBT
|
||||
from .base58 import xpub_to_pub_hex
|
||||
from .errors import (
|
||||
UnknownDeviceError,
|
||||
UnavailableActionError,
|
||||
BAD_ARGUMENT,
|
||||
NOT_IMPLEMENTED,
|
||||
)
|
||||
@ -61,15 +62,16 @@ def find_device(password='', device_type=None, fingerprint=None, expert=False):
|
||||
try:
|
||||
client = get_client(d['type'], d['path'], password, expert)
|
||||
|
||||
master_fpr = d.get('fingerprint', None)
|
||||
if master_fpr is None:
|
||||
master_fpr = client.get_master_fingerprint_hex()
|
||||
if fingerprint:
|
||||
master_fpr = d.get('fingerprint', None)
|
||||
if master_fpr is None:
|
||||
master_fpr = client.get_master_fingerprint_hex()
|
||||
|
||||
if fingerprint and master_fpr != fingerprint:
|
||||
client.close()
|
||||
continue
|
||||
if master_fpr != fingerprint:
|
||||
client.close()
|
||||
continue
|
||||
return client
|
||||
except:
|
||||
except Exception:
|
||||
if client:
|
||||
client.close()
|
||||
pass # Ignore things we wouldn't get fingerprints for
|
||||
@ -206,10 +208,12 @@ def getdescriptors(client, account=0):
|
||||
|
||||
for internal in [False, True]:
|
||||
descriptors = []
|
||||
desc1 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, addr_type=AddressType.PKH, account=account)
|
||||
desc2 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, addr_type=AddressType.SH_WPKH, account=account)
|
||||
desc3 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, addr_type=AddressType.WPKH, account=account)
|
||||
for desc in [desc1, desc2, desc3]:
|
||||
for addr_type in (AddressType.PKH, AddressType.SH_WPKH, AddressType.WPKH):
|
||||
try:
|
||||
desc = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, addr_type=addr_type, account=account)
|
||||
except UnavailableActionError:
|
||||
# Device does not support this address type or network. Skip.
|
||||
continue
|
||||
if not isinstance(desc, Descriptor):
|
||||
return desc
|
||||
descriptors.append(desc.serialize())
|
||||
@ -236,15 +240,18 @@ def displayaddress(client, path=None, desc=None, sh_wpkh=False, wpkh=False, rede
|
||||
if descriptor.sh or descriptor.sh_wsh or descriptor.wsh:
|
||||
path = ''
|
||||
redeem_script = format(80 + int(descriptor.multisig_M), 'x')
|
||||
xpubs_descriptor = False
|
||||
for i in range(0, descriptor.multisig_N):
|
||||
path += descriptor.origin_fingerprint[i] + descriptor.origin_path[i] + ','
|
||||
path += descriptor.origin_fingerprint[i] + descriptor.origin_path[i]
|
||||
if not descriptor.path_suffix[i]:
|
||||
redeem_script += '21' + descriptor.base_key[i]
|
||||
else:
|
||||
return {'error': 'Multisig descriptor must include all pubkeys', 'code': BAD_ARGUMENT}
|
||||
path += descriptor.path_suffix[i]
|
||||
xpubs_descriptor = True
|
||||
path += ','
|
||||
path = path[0:-1]
|
||||
redeem_script += format(80 + descriptor.multisig_N, 'x') + 'ae'
|
||||
return client.display_address(path, descriptor.sh_wpkh or descriptor.sh_wsh, descriptor.wpkh or descriptor.wsh, redeem_script)
|
||||
return client.display_address(path, descriptor.sh_wpkh or descriptor.sh_wsh, descriptor.wpkh or descriptor.wsh, redeem_script, descriptor=descriptor if xpubs_descriptor else None)
|
||||
if descriptor.m_path is None:
|
||||
return {'error': 'Descriptor missing origin info: ' + desc, 'code': BAD_ARGUMENT}
|
||||
if descriptor.origin_fingerprint != client.get_master_fingerprint_hex():
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
# mypy: ignore-errors
|
||||
import re
|
||||
|
||||
# From: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp
|
||||
@ -82,6 +83,12 @@ class Descriptor:
|
||||
if origin_path and not isinstance(origin_path, list):
|
||||
self.m_path_base = "m" + origin_path
|
||||
self.m_path = "m" + origin_path + (path_suffix or "")
|
||||
elif isinstance(origin_path, list):
|
||||
self.m_path_base = []
|
||||
self.m_path = []
|
||||
for i in range(0, len(origin_path)):
|
||||
self.m_path_base.append("m" + origin_path[i])
|
||||
self.m_path.append("m" + origin_path[i] + (path_suffix[i] or ""))
|
||||
|
||||
@classmethod
|
||||
def parse(cls, desc, testnet=False):
|
||||
@ -147,6 +154,9 @@ class Descriptor:
|
||||
origin_path = match.group(2)
|
||||
# Replace h with '
|
||||
origin_path = origin_path.replace('h', '\'')
|
||||
else:
|
||||
origin_fingerprint = origin
|
||||
origin_path = ''
|
||||
|
||||
base_key_and_path_match = re.search(r"\[.*\](\w+)([\d'\/\*]*)", key)
|
||||
else:
|
||||
|
||||
@ -3,5 +3,6 @@ __all__ = [
|
||||
'ledger',
|
||||
'keepkey',
|
||||
'digitalbitbox',
|
||||
'coldcard'
|
||||
'coldcard',
|
||||
'bitbox02',
|
||||
]
|
||||
|
||||
659
hwilib/devices/bitbox02.py
Normal file
659
hwilib/devices/bitbox02.py
Normal file
@ -0,0 +1,659 @@
|
||||
from typing import (
|
||||
cast,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Optional,
|
||||
Union,
|
||||
Tuple,
|
||||
List,
|
||||
Sequence,
|
||||
TypeVar,
|
||||
)
|
||||
from binascii import unhexlify
|
||||
import struct
|
||||
import builtins
|
||||
import sys
|
||||
from functools import wraps
|
||||
|
||||
from ..hwwclient import HardwareWalletClient, Descriptor
|
||||
from ..serializations import (
|
||||
PSBT,
|
||||
CTxOut,
|
||||
is_p2pkh,
|
||||
is_p2wpkh,
|
||||
is_p2wsh,
|
||||
ser_uint256,
|
||||
ser_sig_der,
|
||||
)
|
||||
from ..errors import (
|
||||
HWWError,
|
||||
ActionCanceledError,
|
||||
BadArgumentError,
|
||||
DeviceNotReadyError,
|
||||
UnavailableActionError,
|
||||
DEVICE_NOT_INITIALIZED,
|
||||
handle_errors,
|
||||
common_err_msgs,
|
||||
)
|
||||
|
||||
import hid # type: ignore
|
||||
|
||||
from bitbox02 import util
|
||||
from bitbox02 import bitbox02
|
||||
from bitbox02.communication import (
|
||||
devices,
|
||||
u2fhid,
|
||||
FirmwareVersionOutdatedException,
|
||||
Bitbox02Exception,
|
||||
UserAbortException,
|
||||
HARDENED,
|
||||
ERR_GENERIC,
|
||||
)
|
||||
|
||||
from bitbox02.communication.bitbox_api_protocol import (
|
||||
Platform,
|
||||
BitBox02Edition,
|
||||
BitBoxNoiseConfig,
|
||||
)
|
||||
|
||||
|
||||
class BitBox02Error(UnavailableActionError):
|
||||
def __init__(self, msg: str):
|
||||
"""
|
||||
BitBox02 unexpected error. The BitBox02 does not return give granular error messages,
|
||||
so we give hints to as what could be wrong.
|
||||
"""
|
||||
msg = "Input error: {}. A keypath might be invalid. Supported keypaths are: ".format(
|
||||
msg
|
||||
)
|
||||
msg += "m/49'/0'/<account'> for p2wpkh-p2sh; "
|
||||
msg += "m/84'/0'/<account'> for p2wpkh; "
|
||||
msg += "m/48'/0'/<account'>/2' for p2wsh multisig; "
|
||||
msg += "account can be between 0' and 99'; "
|
||||
msg += "For address keypaths, append /0/<address index> for a receive and /1/<change index> for a change address."
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
ERR_INVALID_INPUT = 101
|
||||
|
||||
PURPOSE_P2WPKH_P2SH = 49 + HARDENED
|
||||
PURPOSE_P2WPKH = 84 + HARDENED
|
||||
PURPOSE_MULTISIG_P2WSH = 48 + HARDENED
|
||||
|
||||
# External GUI tools using hwi.py as a command line tool to integrate hardware wallets usually do
|
||||
# not have an actual terminal for IO.
|
||||
_using_external_gui = not sys.stdout.isatty()
|
||||
if _using_external_gui:
|
||||
_unpaired_errmsg = "Device not paired yet. Please pair using the BitBoxApp, then close the BitBoxApp and try again."
|
||||
else:
|
||||
_unpaired_errmsg = "Device not paired yet. Please use any subcommand to pair"
|
||||
|
||||
|
||||
class SilentNoiseConfig(util.BitBoxAppNoiseConfig):
|
||||
"""
|
||||
Used during `enumerate()`. Raises an exception if the device is unpaired.
|
||||
Attestation check is silent.
|
||||
|
||||
Rationale: enumerate() should not show any dialogs.
|
||||
"""
|
||||
|
||||
def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool:
|
||||
raise DeviceNotReadyError(_unpaired_errmsg)
|
||||
|
||||
def attestation_check(self, result: bool) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class CLINoiseConfig(util.BitBoxAppNoiseConfig):
|
||||
""" Noise pairing and attestation check handling in the terminal (stdin/stdout) """
|
||||
|
||||
def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool:
|
||||
if _using_external_gui:
|
||||
# The user can't see the pairing in the terminal. The
|
||||
# output format is also not appropriate for parsing by
|
||||
# external tools doing inter process communication using
|
||||
# stdin/stdout. For now, we direct the user to pair in the
|
||||
# BitBoxApp instead.
|
||||
raise DeviceNotReadyError(_unpaired_errmsg)
|
||||
|
||||
print("Please compare and confirm the pairing code on your BitBox02:")
|
||||
print(code)
|
||||
if not device_response():
|
||||
return False
|
||||
return input("Accept pairing? [y]/n: ").strip() != "n"
|
||||
|
||||
def attestation_check(self, result: bool) -> None:
|
||||
if result:
|
||||
sys.stderr.write("BitBox02 attestation check PASSED\n")
|
||||
else:
|
||||
sys.stderr.write("BitBox02 attestation check FAILED\n")
|
||||
sys.stderr.write(
|
||||
"Your BitBox02 might not be genuine. Please contact support@shiftcrypto.ch if the problem persists.\n"
|
||||
)
|
||||
|
||||
|
||||
def _parse_path(nstr: str) -> Sequence[int]:
|
||||
"""
|
||||
Adapted from trezorlib.tools.parse_path.
|
||||
Convert BIP32 path string to list of uint32 integers with hardened flags.
|
||||
Several conventions are supported to set the hardened flag: -1, 1', 1h
|
||||
|
||||
e.g.: "0/1h/1" -> [0, 0x80000001, 1]
|
||||
|
||||
:param nstr: path string
|
||||
:return: list of integers
|
||||
"""
|
||||
if not nstr:
|
||||
return []
|
||||
|
||||
n = nstr.split("/")
|
||||
|
||||
# m/a/b/c => a/b/c
|
||||
if n[0] == "m":
|
||||
n = n[1:]
|
||||
|
||||
def str_to_harden(x: str) -> int:
|
||||
if x.startswith("-"):
|
||||
return abs(int(x)) + HARDENED
|
||||
elif x.endswith(("h", "'")):
|
||||
return int(x[:-1]) + HARDENED
|
||||
else:
|
||||
return int(x)
|
||||
|
||||
try:
|
||||
return [str_to_harden(x) for x in n]
|
||||
except Exception:
|
||||
raise ValueError("Invalid BIP32 path", nstr)
|
||||
|
||||
|
||||
def enumerate(password: str = "") -> List[Dict[str, object]]:
|
||||
"""
|
||||
Enumerate all BitBox02 devices. Bootloaders excluded.
|
||||
"""
|
||||
result = []
|
||||
for device_info in devices.get_any_bitbox02s():
|
||||
path = device_info["path"].decode()
|
||||
client = Bitbox02Client(path)
|
||||
client.set_noise_config(SilentNoiseConfig())
|
||||
d_data: Dict[str, object] = {}
|
||||
bb02 = None
|
||||
with handle_errors(common_err_msgs["enumerate"], d_data):
|
||||
bb02 = client.init(expect_initialized=None)
|
||||
version, platform, edition, unlocked = bitbox02.BitBox02.get_info(
|
||||
client.transport
|
||||
)
|
||||
if platform != Platform.BITBOX02:
|
||||
client.close()
|
||||
continue
|
||||
if edition not in (BitBox02Edition.MULTI, BitBox02Edition.BTCONLY):
|
||||
client.close()
|
||||
continue
|
||||
|
||||
assert isinstance(edition, BitBox02Edition)
|
||||
|
||||
d_data.update(
|
||||
{
|
||||
"type": "bitbox02",
|
||||
"path": path,
|
||||
"model": {
|
||||
BitBox02Edition.MULTI: "bitbox02_multi",
|
||||
BitBox02Edition.BTCONLY: "bitbox02_btconly",
|
||||
}[edition],
|
||||
"needs_pin_sent": False,
|
||||
"needs_passphrase_sent": False,
|
||||
}
|
||||
)
|
||||
|
||||
if bb02 is not None:
|
||||
with handle_errors(common_err_msgs["enumerate"], d_data):
|
||||
if not bb02.device_info()["initialized"]:
|
||||
raise DeviceNotReadyError(
|
||||
"BitBox02 is not initialized. Please initialize it using the BitBoxApp."
|
||||
)
|
||||
elif not unlocked:
|
||||
raise DeviceNotReadyError(
|
||||
"Please load wallet to unlock."
|
||||
if _using_external_gui
|
||||
else "Please use any subcommand to unlock"
|
||||
)
|
||||
d_data["fingerprint"] = client.get_master_fingerprint_hex()
|
||||
|
||||
result.append(d_data)
|
||||
|
||||
client.close()
|
||||
return result
|
||||
|
||||
|
||||
T = TypeVar("T", bound=Callable[..., Any])
|
||||
|
||||
|
||||
def bitbox02_exception(f: T) -> T:
|
||||
"""
|
||||
Maps bitbox02 library exceptions into a HWI exceptions.
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def func(*args, **kwargs): # type: ignore
|
||||
""" Wraps f, mapping exceptions. """
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except UserAbortException:
|
||||
raise ActionCanceledError("{} canceled".format(f.__name__))
|
||||
except Bitbox02Exception as exc:
|
||||
if exc.code in (ERR_GENERIC, ERR_INVALID_INPUT):
|
||||
raise BitBox02Error(str(exc))
|
||||
raise exc
|
||||
except FirmwareVersionOutdatedException as exc:
|
||||
raise DeviceNotReadyError(str(exc))
|
||||
|
||||
return cast(T, func)
|
||||
|
||||
|
||||
# This class extends the HardwareWalletClient for BitBox02 specific things
|
||||
class Bitbox02Client(HardwareWalletClient):
|
||||
def __init__(self, path: str, password: str = "", expert: bool = False) -> None:
|
||||
"""
|
||||
Initializes a new BitBox02 client instance.
|
||||
"""
|
||||
super().__init__(path, password=password, expert=expert)
|
||||
if password:
|
||||
raise BadArgumentError(
|
||||
"The BitBox02 does not accept a passphrase from the host. Please enable the passphrase option and enter the passphrase on the device during unlock."
|
||||
)
|
||||
|
||||
hid_device = hid.device()
|
||||
hid_device.open_path(path.encode())
|
||||
self.transport = u2fhid.U2FHid(hid_device)
|
||||
self.device_path = path
|
||||
|
||||
# use self.init() to access self.bb02.
|
||||
self.bb02: Optional[bitbox02.BitBox02] = None
|
||||
|
||||
self.noise_config: BitBoxNoiseConfig = CLINoiseConfig()
|
||||
|
||||
def set_noise_config(self, noise_config: BitBoxNoiseConfig) -> None:
|
||||
self.noise_config = noise_config
|
||||
|
||||
def init(self, expect_initialized: Optional[bool] = True) -> bitbox02.BitBox02:
|
||||
if self.bb02 is not None:
|
||||
return self.bb02
|
||||
|
||||
for device_info in devices.get_any_bitbox02s():
|
||||
if device_info["path"].decode() == self.device_path:
|
||||
bb02 = bitbox02.BitBox02(
|
||||
transport=self.transport,
|
||||
device_info=device_info,
|
||||
noise_config=self.noise_config,
|
||||
)
|
||||
try:
|
||||
bb02.check_min_version()
|
||||
except FirmwareVersionOutdatedException as exc:
|
||||
sys.stderr.write("WARNING: {}\n".format(exc))
|
||||
raise
|
||||
self.bb02 = bb02
|
||||
is_initialized = bb02.device_info()["initialized"]
|
||||
if expect_initialized is not None:
|
||||
if expect_initialized:
|
||||
if not is_initialized:
|
||||
raise HWWError(
|
||||
"The BitBox02 must be initialized first.",
|
||||
DEVICE_NOT_INITIALIZED,
|
||||
)
|
||||
elif is_initialized:
|
||||
raise UnavailableActionError(
|
||||
"The BitBox02 must be wiped before setup."
|
||||
)
|
||||
|
||||
return bb02
|
||||
raise Exception(
|
||||
"Could not find the hid device info for path {}".format(self.device_path)
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
self.transport.close()
|
||||
|
||||
def get_master_fingerprint_hex(self) -> str:
|
||||
"""
|
||||
HWI by default retrieves the fingerprint at m/ by getting the xpub at m/0', which contains the parent fingerprint.
|
||||
The BitBox02 does not support querying arbitrary keypaths, but has an api call return the fingerprint at m/.
|
||||
"""
|
||||
bb02 = self.init()
|
||||
return bb02.root_fingerprint().hex()
|
||||
|
||||
def prompt_pin(self) -> Dict[str, Union[bool, str, int]]:
|
||||
raise UnavailableActionError(
|
||||
"The BitBox02 does not need a PIN sent from the host"
|
||||
)
|
||||
|
||||
def send_pin(self, pin: str) -> Dict[str, Union[bool, str, int]]:
|
||||
raise UnavailableActionError(
|
||||
"The BitBox02 does not need a PIN sent from the host"
|
||||
)
|
||||
|
||||
def _get_coin(self) -> bitbox02.btc.BTCCoin:
|
||||
if self.is_testnet:
|
||||
return bitbox02.btc.TBTC
|
||||
return bitbox02.btc.BTC
|
||||
|
||||
def _get_xpub(self, keypath: Sequence[int]) -> str:
|
||||
xpub_type = (
|
||||
bitbox02.btc.BTCPubRequest.TPUB
|
||||
if self.is_testnet
|
||||
else bitbox02.btc.BTCPubRequest.XPUB
|
||||
)
|
||||
return self.init().btc_xpub(
|
||||
keypath, coin=self._get_coin(), xpub_type=xpub_type, display=False
|
||||
)
|
||||
|
||||
def get_pubkey_at_path(self, bip32_path: str) -> Dict[str, str]:
|
||||
path_uint32s = _parse_path(bip32_path)
|
||||
try:
|
||||
xpub = self._get_xpub(path_uint32s)
|
||||
except Bitbox02Exception as exc:
|
||||
raise BitBox02Error(str(exc))
|
||||
return {"xpub": xpub}
|
||||
|
||||
@bitbox02_exception
|
||||
def display_address(
|
||||
self,
|
||||
bip32_path: str,
|
||||
p2sh_p2wpkh: bool,
|
||||
bech32: bool,
|
||||
redeem_script: Optional[str] = None,
|
||||
descriptor: Optional[Descriptor] = None,
|
||||
) -> Dict[str, str]:
|
||||
if redeem_script:
|
||||
raise NotImplementedError("BitBox02 multisig not integrated into HWI yet")
|
||||
|
||||
if p2sh_p2wpkh:
|
||||
script_config = bitbox02.btc.BTCScriptConfig(
|
||||
simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH
|
||||
)
|
||||
elif bech32:
|
||||
script_config = bitbox02.btc.BTCScriptConfig(
|
||||
simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH
|
||||
)
|
||||
else:
|
||||
raise UnavailableActionError(
|
||||
"The BitBox02 does not support legacy p2pkh addresses"
|
||||
)
|
||||
address = self.init().btc_address(
|
||||
_parse_path(bip32_path),
|
||||
coin=self._get_coin(),
|
||||
script_config=script_config,
|
||||
display=True,
|
||||
)
|
||||
return {"address": address}
|
||||
|
||||
@bitbox02_exception
|
||||
def sign_tx(self, psbt: PSBT) -> Dict[str, str]:
|
||||
def find_our_key(
|
||||
keypaths: Dict[bytes, Sequence[int]]
|
||||
) -> Tuple[Optional[bytes], Optional[Sequence[int]]]:
|
||||
"""
|
||||
Keypaths is a map of pubkey to hd keypath, where the first element in the keypath is the master fingerprint. We attempt to find the key which belongs to the BitBox02 by matching the fingerprint, and then matching the pubkey.
|
||||
Returns the pubkey and the keypath, without the fingerprint.
|
||||
"""
|
||||
for pubkey, keypath_with_fingerprint in keypaths.items():
|
||||
fp, keypath = keypath_with_fingerprint[0], keypath_with_fingerprint[1:]
|
||||
# Cheap check if the key is ours.
|
||||
if fp != master_fp:
|
||||
continue
|
||||
|
||||
# Expensive check if the key is ours.
|
||||
# TODO: check for fingerprint collision
|
||||
# keypath_account = keypath[:-2]
|
||||
|
||||
return pubkey, keypath
|
||||
return None, None
|
||||
|
||||
def get_simple_type(
|
||||
output: CTxOut, redeem_script: bytes
|
||||
) -> bitbox02.btc.BTCScriptConfig.SimpleType:
|
||||
if is_p2pkh(output.scriptPubKey):
|
||||
raise BadArgumentError(
|
||||
"The BitBox02 does not support legacy p2pkh scripts"
|
||||
)
|
||||
if is_p2wpkh(output.scriptPubKey):
|
||||
return bitbox02.btc.BTCScriptConfig.P2WPKH
|
||||
if output.is_p2sh() and is_p2wpkh(redeem_script):
|
||||
return bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH
|
||||
raise BadArgumentError(
|
||||
"Input script type not recognized of input {}.".format(input_index)
|
||||
)
|
||||
|
||||
master_fp = struct.unpack("<I", unhexlify(self.get_master_fingerprint_hex()))[0]
|
||||
|
||||
inputs: List[bitbox02.BTCInputType] = []
|
||||
|
||||
bip44_account = None
|
||||
|
||||
# One pubkey per input. The pubkey identifies the key per input with which we sign. There
|
||||
# must be exactly one pubkey per input that belongs to the BitBox02.
|
||||
found_pubkeys: List[bytes] = []
|
||||
|
||||
for input_index, (psbt_in, tx_in) in builtins.enumerate(
|
||||
zip(psbt.inputs, psbt.tx.vin)
|
||||
):
|
||||
if psbt_in.sighash and psbt_in.sighash != 1:
|
||||
raise BadArgumentError(
|
||||
"The BitBox02 only supports SIGHASH_ALL. Found sighash: {}".format(
|
||||
psbt_in.sighash
|
||||
)
|
||||
)
|
||||
|
||||
utxo = None
|
||||
prevtx = None
|
||||
|
||||
# psbt_in.witness_utxo was originally used for segwit utxo's, but since it was
|
||||
# discovered that the amounts are not correctly committed to in the segwit sighash, the
|
||||
# full prevtx (non_witness_utxo) is supplied for both segwit and non-segwit inputs.
|
||||
# See
|
||||
# - https://medium.com/shiftcrypto/bitbox-app-firmware-update-6-2020-c70f733a5330
|
||||
# - https://blog.trezor.io/details-of-firmware-updates-for-trezor-one-version-1-9-1-and-trezor-model-t-version-2-3-1-1eba8f60f2dd.
|
||||
# - https://github.com/zkSNACKs/WalletWasabi/pull/3822
|
||||
# The BitBox02 for now requires the prevtx, at least until Taproot activates.
|
||||
|
||||
if psbt_in.non_witness_utxo:
|
||||
if tx_in.prevout.hash != psbt_in.non_witness_utxo.sha256:
|
||||
raise BadArgumentError(
|
||||
"Input {} has a non_witness_utxo with the wrong hash".format(
|
||||
input_index
|
||||
)
|
||||
)
|
||||
utxo = psbt_in.non_witness_utxo.vout[tx_in.prevout.n]
|
||||
prevtx = psbt_in.non_witness_utxo
|
||||
elif psbt_in.witness_utxo:
|
||||
utxo = psbt_in.witness_utxo
|
||||
if utxo is None:
|
||||
raise BadArgumentError("No utxo found for input {}".format(input_index))
|
||||
if prevtx is None:
|
||||
raise BadArgumentError(
|
||||
"Previous transaction missing for input {}".format(input_index)
|
||||
)
|
||||
|
||||
found_pubkey, keypath = find_our_key(psbt_in.hd_keypaths)
|
||||
if not found_pubkey:
|
||||
raise BadArgumentError("No key found for input {}".format(input_index))
|
||||
assert keypath is not None
|
||||
found_pubkeys.append(found_pubkey)
|
||||
|
||||
# TOOD: validate keypath
|
||||
|
||||
if bip44_account is None:
|
||||
bip44_account = keypath[2]
|
||||
elif bip44_account != keypath[2]:
|
||||
raise BadArgumentError(
|
||||
"The bip44 account index must be the same for all inputs and changes"
|
||||
)
|
||||
|
||||
simple_type = get_simple_type(utxo, psbt_in.redeem_script)
|
||||
|
||||
script_config_index_map = {
|
||||
bitbox02.btc.BTCScriptConfig.P2WPKH: 0,
|
||||
bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH: 1,
|
||||
}
|
||||
|
||||
inputs.append(
|
||||
{
|
||||
"prev_out_hash": ser_uint256(tx_in.prevout.hash),
|
||||
"prev_out_index": tx_in.prevout.n,
|
||||
"prev_out_value": utxo.nValue,
|
||||
"sequence": tx_in.nSequence,
|
||||
"keypath": keypath,
|
||||
"script_config_index": script_config_index_map[simple_type],
|
||||
"prev_tx": {
|
||||
"version": prevtx.nVersion,
|
||||
"locktime": prevtx.nLockTime,
|
||||
"inputs": [
|
||||
{
|
||||
"prev_out_hash": ser_uint256(prev_in.prevout.hash),
|
||||
"prev_out_index": prev_in.prevout.n,
|
||||
"signature_script": prev_in.scriptSig,
|
||||
"sequence": prev_in.nSequence,
|
||||
}
|
||||
for prev_in in prevtx.vin
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"value": prev_out.nValue,
|
||||
"pubkey_script": prev_out.scriptPubKey,
|
||||
}
|
||||
for prev_out in prevtx.vout
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
outputs: List[bitbox02.BTCOutputType] = []
|
||||
for output_index, (psbt_out, tx_out) in builtins.enumerate(
|
||||
zip(psbt.outputs, psbt.tx.vout)
|
||||
):
|
||||
_, keypath = find_our_key(psbt_out.hd_keypaths)
|
||||
is_change = keypath and keypath[-2] == 1
|
||||
if is_change:
|
||||
assert keypath is not None
|
||||
simple_type = get_simple_type(tx_out, psbt_out.redeem_script)
|
||||
outputs.append(
|
||||
bitbox02.BTCOutputInternal(
|
||||
keypath=keypath,
|
||||
value=tx_out.nValue,
|
||||
script_config_index=script_config_index_map[simple_type],
|
||||
)
|
||||
)
|
||||
else:
|
||||
if tx_out.is_p2pkh():
|
||||
output_type = bitbox02.btc.P2PKH
|
||||
output_hash = tx_out.scriptPubKey[3:23]
|
||||
elif is_p2wpkh(tx_out.scriptPubKey):
|
||||
output_type = bitbox02.btc.P2WPKH
|
||||
output_hash = tx_out.scriptPubKey[2:]
|
||||
elif tx_out.is_p2sh():
|
||||
output_type = bitbox02.btc.P2SH
|
||||
output_hash = tx_out.scriptPubKey[2:22]
|
||||
elif is_p2wsh(tx_out.scriptPubKey):
|
||||
output_type = bitbox02.btc.P2WSH
|
||||
output_hash = tx_out.scriptPubKey[2:]
|
||||
else:
|
||||
raise BadArgumentError(
|
||||
"Output type not recognized of output {}".format(output_index)
|
||||
)
|
||||
|
||||
outputs.append(
|
||||
bitbox02.BTCOutputExternal(
|
||||
output_type=output_type,
|
||||
output_hash=output_hash,
|
||||
value=tx_out.nValue,
|
||||
)
|
||||
)
|
||||
|
||||
assert bip44_account is not None
|
||||
|
||||
bip44_network = 1 + HARDENED if self.is_testnet else 0 + HARDENED
|
||||
sigs = self.init().btc_sign(
|
||||
bitbox02.btc.TBTC if self.is_testnet else bitbox02.btc.BTC,
|
||||
[
|
||||
bitbox02.btc.BTCScriptConfigWithKeypath(
|
||||
script_config=bitbox02.btc.BTCScriptConfig(
|
||||
simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH
|
||||
),
|
||||
keypath=[84 + HARDENED, bip44_network, bip44_account],
|
||||
),
|
||||
bitbox02.btc.BTCScriptConfigWithKeypath(
|
||||
script_config=bitbox02.btc.BTCScriptConfig(
|
||||
simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH
|
||||
),
|
||||
keypath=[49 + HARDENED, bip44_network, bip44_account],
|
||||
),
|
||||
],
|
||||
inputs=inputs,
|
||||
outputs=outputs,
|
||||
locktime=psbt.tx.nLockTime,
|
||||
version=psbt.tx.nVersion,
|
||||
)
|
||||
|
||||
for (_, sig), pubkey, psbt_in in zip(sigs, found_pubkeys, psbt.inputs):
|
||||
r, s = sig[:32], sig[32:64]
|
||||
# ser_sig_der() adds SIGHASH_ALL
|
||||
psbt_in.partial_sigs[pubkey] = ser_sig_der(r, s)
|
||||
|
||||
return {"psbt": psbt.serialize()}
|
||||
|
||||
def sign_message(
|
||||
self, message: Union[str, bytes], bip32_path: str
|
||||
) -> Dict[str, str]:
|
||||
raise UnavailableActionError("The BitBox02 does not support 'signmessage'")
|
||||
|
||||
@bitbox02_exception
|
||||
def toggle_passphrase(self) -> Dict[str, Union[bool, str, int]]:
|
||||
bb02 = self.init()
|
||||
info = bb02.device_info()
|
||||
if info["mnemonic_passphrase_enabled"]:
|
||||
bb02.disable_mnemonic_passphrase()
|
||||
else:
|
||||
bb02.enable_mnemonic_passphrase()
|
||||
return {"success": True}
|
||||
|
||||
@bitbox02_exception
|
||||
def setup_device(
|
||||
self, label: str = "", passphrase: str = ""
|
||||
) -> Dict[str, Union[bool, str, int]]:
|
||||
if passphrase:
|
||||
raise UnavailableActionError(
|
||||
"Passphrase not needed when setting up a BitBox02."
|
||||
)
|
||||
|
||||
bb02 = self.init(expect_initialized=False)
|
||||
|
||||
if label:
|
||||
bb02.set_device_name(label)
|
||||
if not bb02.set_password():
|
||||
return {"success": False}
|
||||
return {"success": bb02.create_backup()}
|
||||
|
||||
@bitbox02_exception
|
||||
def wipe_device(self) -> Dict[str, Union[bool, str, int]]:
|
||||
return {"success": self.init().reset()}
|
||||
|
||||
@bitbox02_exception
|
||||
def backup_device(
|
||||
self, label: str = "", passphrase: str = ""
|
||||
) -> Dict[str, Union[bool, str, int]]:
|
||||
if label or passphrase:
|
||||
raise UnavailableActionError(
|
||||
"Label/passphrase not needed when exporting mnemonic from the BitBox02."
|
||||
)
|
||||
|
||||
return {"success": self.init().show_mnemonic()}
|
||||
|
||||
@bitbox02_exception
|
||||
def restore_device(
|
||||
self, label: str = "", word_count: int = 24
|
||||
) -> Dict[str, Union[bool, str, int]]:
|
||||
bb02 = self.init(expect_initialized=False)
|
||||
|
||||
if label:
|
||||
bb02.set_device_name(label)
|
||||
|
||||
return {"success": bb02.restore_from_mnemonic()}
|
||||
@ -101,14 +101,14 @@ class bitcoinTransaction:
|
||||
inputSize = readVarint(data, offset)
|
||||
offset += inputSize['size']
|
||||
numInputs = inputSize['value']
|
||||
for i in range(numInputs):
|
||||
for _ in range(numInputs):
|
||||
tmp = { 'buffer': data, 'offset' : offset}
|
||||
self.inputs.append(bitcoinInput(tmp))
|
||||
offset = tmp['offset']
|
||||
outputSize = readVarint(data, offset)
|
||||
offset += outputSize['size']
|
||||
numOutputs = outputSize['value']
|
||||
for i in range(numOutputs):
|
||||
for _ in range(numOutputs):
|
||||
tmp = { 'buffer': data, 'offset' : offset}
|
||||
self.outputs.append(bitcoinOutput(tmp))
|
||||
offset = tmp['offset']
|
||||
|
||||
@ -84,7 +84,7 @@ class btchip:
|
||||
self.scriptBlockLength = 50
|
||||
else:
|
||||
self.scriptBlockLength = 255
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def getWalletPublicKey(self, path, showOnScreen=False, segwit=False, segwitNative=False, cashAddr=False):
|
||||
@ -271,7 +271,7 @@ class btchip:
|
||||
response = self.dongle.exchange(bytearray(apdu))
|
||||
offset += dataLength
|
||||
alternateEncoding = True
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
if not alternateEncoding:
|
||||
apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE, 0x02, 0x00 ]
|
||||
|
||||
@ -142,7 +142,7 @@ class HIDDongleHIDAPI(Dongle, DongleWait):
|
||||
if self.opened:
|
||||
try:
|
||||
self.device.close()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
self.opened = False
|
||||
|
||||
@ -155,7 +155,7 @@ class DongleServer(Dongle):
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
self.socket.connect((self.server, self.port))
|
||||
except:
|
||||
except Exception:
|
||||
raise BTChipException("Proxy connection failed")
|
||||
|
||||
def exchange(self, apdu, timeout=20000):
|
||||
@ -175,5 +175,5 @@ class DongleServer(Dongle):
|
||||
def close(self):
|
||||
try:
|
||||
self.socket.close()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@ -159,10 +159,10 @@ class ColdcardDevice:
|
||||
print("Rx [%2d]: %r" % (len(resp), b2a_hex(bytes(resp))))
|
||||
|
||||
return CCProtocolUnpacker.decode(resp)
|
||||
except CCProtoError as e:
|
||||
except CCProtoError:
|
||||
if expect_errors: raise
|
||||
raise
|
||||
except:
|
||||
except Exception:
|
||||
#print("Corrupt response: %r" % resp)
|
||||
raise
|
||||
|
||||
@ -258,7 +258,7 @@ class ColdcardDevice:
|
||||
|
||||
# If Pycoin is not available, do it using ecdsa
|
||||
from ecdsa import BadSignatureError, SECP256k1, VerifyingKey
|
||||
pubkey, chaincode = decode_xpub(expected_xpub)
|
||||
pubkey, _ = decode_xpub(expected_xpub)
|
||||
vk = VerifyingKey.from_string(get_pubkey_string(pubkey), curve=SECP256k1)
|
||||
try:
|
||||
ok = vk.verify_digest(sig[1:], self.session_key)
|
||||
@ -376,7 +376,8 @@ class UnixSimulatorPipe:
|
||||
self.pipe.close()
|
||||
try:
|
||||
os.unlink(self.pipe_name)
|
||||
except: pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get_serial_number_string(self):
|
||||
return 'simulator'
|
||||
|
||||
@ -24,14 +24,14 @@ def dfu_parse(fd):
|
||||
|
||||
assert dfu_prefix.signature == b'DfuSe', "Not a DFU file (bad magic)"
|
||||
|
||||
for idx in range(dfu_prefix.targets):
|
||||
for _ in range(dfu_prefix.targets):
|
||||
|
||||
prefix = consume(fd, 'Target', '<6sBI255s2I',
|
||||
'signature altsetting named name size elements')
|
||||
|
||||
#print("target%d: %r" % (idx, prefix))
|
||||
|
||||
for ei in range(prefix.elements):
|
||||
for _ in range(prefix.elements):
|
||||
# Decode target prefix
|
||||
# < little endian
|
||||
# I uint32_t element address
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# Coldcard interaction script
|
||||
|
||||
from typing import Dict, Union
|
||||
|
||||
from ..hwwclient import HardwareWalletClient
|
||||
from ..errors import (
|
||||
ActionCanceledError,
|
||||
@ -122,7 +124,7 @@ class ColdcardClient(HardwareWalletClient):
|
||||
if our_keys > passes:
|
||||
passes = our_keys
|
||||
|
||||
for i in range(0, passes):
|
||||
for _ in range(passes):
|
||||
# Get psbt in hex and then make binary
|
||||
fd = io.BytesIO(base64.b64decode(tx.serialize()))
|
||||
|
||||
@ -174,15 +176,18 @@ class ColdcardClient(HardwareWalletClient):
|
||||
tx.deserialize(base64.b64encode(result).decode())
|
||||
return {'psbt': tx.serialize()}
|
||||
|
||||
# Must return a base64 encoded string with the signed message
|
||||
# The message can be any string. keypath is the bip 32 derivation path for the key to sign with
|
||||
@coldcard_exception
|
||||
def sign_message(self, message, keypath):
|
||||
def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, str]:
|
||||
self.device.check_mitm()
|
||||
keypath = keypath.replace('h', '\'')
|
||||
keypath = keypath.replace('H', '\'')
|
||||
|
||||
ok = self.device.send_recv(CCProtocolPacker.sign_message(message.encode(), keypath, AF_CLASSIC), timeout=None)
|
||||
msg = message
|
||||
if not isinstance(message, bytes):
|
||||
msg = message.encode()
|
||||
ok = self.device.send_recv(
|
||||
CCProtocolPacker.sign_message(msg, keypath, AF_CLASSIC), timeout=None
|
||||
)
|
||||
assert ok is None
|
||||
if self.device.is_simulator:
|
||||
self.device.send_recv(CCProtocolPacker.sim_keypress(b'y'))
|
||||
@ -198,14 +203,14 @@ class ColdcardClient(HardwareWalletClient):
|
||||
if len(done) != 2:
|
||||
raise DeviceFailureError('Failed: %r' % done)
|
||||
|
||||
addr, raw = done
|
||||
_, raw = done
|
||||
|
||||
sig = str(base64.b64encode(raw), 'ascii').replace('\n', '')
|
||||
return {"signature": sig}
|
||||
|
||||
# Display address of specified type on the device.
|
||||
@coldcard_exception
|
||||
def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None):
|
||||
def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, descriptor=None):
|
||||
self.device.check_mitm()
|
||||
keypath = keypath.replace('h', '\'')
|
||||
keypath = keypath.replace('H', '\'')
|
||||
|
||||
@ -13,6 +13,7 @@ import logging
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
from typing import Dict, Union
|
||||
|
||||
from ..hwwclient import HardwareWalletClient
|
||||
from ..errors import (
|
||||
@ -512,14 +513,15 @@ class DigitalbitboxClient(HardwareWalletClient):
|
||||
|
||||
return {'psbt': tx.serialize()}
|
||||
|
||||
# Must return a base64 encoded string with the signed message
|
||||
# The message can be any string
|
||||
@digitalbitbox_exception
|
||||
def sign_message(self, message, keypath):
|
||||
def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, str]:
|
||||
to_hash = b""
|
||||
to_hash += self.message_magic
|
||||
to_hash += ser_compact_size(len(message))
|
||||
to_hash += message.encode()
|
||||
if isinstance(message, bytes):
|
||||
to_hash += message
|
||||
else:
|
||||
to_hash += message.encode()
|
||||
|
||||
hashed_message = hash256(to_hash)
|
||||
|
||||
@ -549,7 +551,7 @@ class DigitalbitboxClient(HardwareWalletClient):
|
||||
return {"signature": base64.b64encode(compact_sig).decode('utf-8')}
|
||||
|
||||
# Display address of specified type on the device.
|
||||
def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None):
|
||||
def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, descriptor=None):
|
||||
raise UnavailableActionError('The Digital Bitbox does not have a screen to display addresses on')
|
||||
|
||||
# Setup a new device
|
||||
@ -629,7 +631,7 @@ def enumerate(password=''):
|
||||
dev.send_recv(b'{"device" : "info"}')
|
||||
devices.append({'path': b'udp:127.0.0.1:35345', 'interface_number': 0})
|
||||
dev.close()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
for d in devices:
|
||||
if ('interface_number' in d and d['interface_number'] == 0
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# Ledger interaction script
|
||||
|
||||
from typing import Dict, Union
|
||||
|
||||
from ..hwwclient import HardwareWalletClient
|
||||
from ..errors import (
|
||||
ActionCanceledError,
|
||||
@ -38,10 +40,14 @@ import re
|
||||
SIMULATOR_PATH = 'tcp:127.0.0.1:9999'
|
||||
|
||||
LEDGER_VENDOR_ID = 0x2c97
|
||||
LEDGER_DEVICE_IDS = [
|
||||
0x0001, # Ledger Nano S
|
||||
0x0004, # Ledger Nano X
|
||||
]
|
||||
LEDGER_MODEL_IDS = {
|
||||
0x10: "ledger_nano_s",
|
||||
0x40: "ledger_nano_x"
|
||||
}
|
||||
LEDGER_LEGACY_PRODUCT_IDS = {
|
||||
0x0001: "ledger_nano_s",
|
||||
0x0004: "ledger_nano_x"
|
||||
}
|
||||
|
||||
# minimal checking of string keypath
|
||||
def check_keypath(key_path):
|
||||
@ -311,13 +317,14 @@ class LedgerClient(HardwareWalletClient):
|
||||
# Send PSBT back
|
||||
return {'psbt': tx.serialize()}
|
||||
|
||||
# Must return a base64 encoded string with the signed message
|
||||
# The message can be any string
|
||||
@ledger_exception
|
||||
def sign_message(self, message, keypath):
|
||||
def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, str]:
|
||||
if not check_keypath(keypath):
|
||||
raise BadArgumentError("Invalid keypath")
|
||||
message = bytearray(message, 'utf-8')
|
||||
if isinstance(message, str):
|
||||
message = bytearray(message, 'utf-8')
|
||||
else:
|
||||
message = bytearray(message)
|
||||
keypath = keypath[2:]
|
||||
# First display on screen what address you're signing for
|
||||
self.app.getWalletPublicKey(keypath, True)
|
||||
@ -340,7 +347,7 @@ class LedgerClient(HardwareWalletClient):
|
||||
|
||||
# Display address of specified type on the device. Only supports single-key based addresses.
|
||||
@ledger_exception
|
||||
def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None):
|
||||
def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, descriptor=None):
|
||||
if not check_keypath(keypath):
|
||||
raise BadArgumentError("Invalid keypath")
|
||||
if redeem_script is not None:
|
||||
@ -383,9 +390,8 @@ class LedgerClient(HardwareWalletClient):
|
||||
def enumerate(password=''):
|
||||
results = []
|
||||
devices = []
|
||||
for device_id in LEDGER_DEVICE_IDS:
|
||||
devices.extend(hid.enumerate(LEDGER_VENDOR_ID, device_id))
|
||||
devices.append({'path': SIMULATOR_PATH.encode(), 'interface_number': 0, 'product_id': 1})
|
||||
devices.extend(hid.enumerate(LEDGER_VENDOR_ID, 0))
|
||||
devices.append({'path': SIMULATOR_PATH.encode(), 'interface_number': 0, 'product_id': 0x1000})
|
||||
|
||||
for d in devices:
|
||||
if ('interface_number' in d and d['interface_number'] == 0
|
||||
@ -394,7 +400,13 @@ def enumerate(password=''):
|
||||
|
||||
path = d['path'].decode()
|
||||
d_data['type'] = 'ledger'
|
||||
d_data['model'] = 'ledger_nano_x' if d['product_id'] == 0x0004 else 'ledger_nano_s'
|
||||
model = d['product_id'] >> 8
|
||||
if model in LEDGER_MODEL_IDS.keys():
|
||||
d_data['model'] = LEDGER_MODEL_IDS[model]
|
||||
elif d['product_id'] in LEDGER_LEGACY_PRODUCT_IDS.keys():
|
||||
d_data['model'] = LEDGER_LEGACY_PRODUCT_IDS[d['product_id']]
|
||||
else:
|
||||
continue
|
||||
d_data['path'] = path
|
||||
|
||||
if path == SIMULATOR_PATH:
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# Trezor interaction script
|
||||
|
||||
from typing import Dict, Union
|
||||
|
||||
from ..hwwclient import HardwareWalletClient
|
||||
from ..errors import (
|
||||
ActionCanceledError,
|
||||
@ -257,7 +259,7 @@ class TrezorClient(HardwareWalletClient):
|
||||
if is_ms:
|
||||
# Add to txinputtype
|
||||
txinputtype.multisig = multisig
|
||||
if not psbt_in.witness_utxo:
|
||||
if not is_wit:
|
||||
if utxo.is_p2sh:
|
||||
txinputtype.script_type = proto.InputScriptType.SPENDMULTISIG
|
||||
else:
|
||||
@ -399,10 +401,8 @@ class TrezorClient(HardwareWalletClient):
|
||||
|
||||
return {'psbt': tx.serialize()}
|
||||
|
||||
# Must return a base64 encoded string with the signed message
|
||||
# The message can be any string
|
||||
@trezor_exception
|
||||
def sign_message(self, message, keypath):
|
||||
def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, str]:
|
||||
self._check_unlocked()
|
||||
path = tools.parse_path(keypath)
|
||||
result = btc.sign_message(self.client, self.coin_name, path, message)
|
||||
@ -410,11 +410,20 @@ class TrezorClient(HardwareWalletClient):
|
||||
|
||||
# Display address of specified type on the device.
|
||||
@trezor_exception
|
||||
def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None):
|
||||
def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, descriptor=None):
|
||||
self._check_unlocked()
|
||||
|
||||
# descriptor means multisig with xpubs
|
||||
if descriptor:
|
||||
pubkeys = []
|
||||
xpub = ExtendedKey()
|
||||
for i in range(0, descriptor.multisig_N):
|
||||
xpub.deserialize(descriptor.base_key[i])
|
||||
hd_node = proto.HDNodeType(depth=xpub.depth, fingerprint=int.from_bytes(xpub.parent_fingerprint, 'big'), child_num=xpub.child_num, chain_code=xpub.chaincode, public_key=xpub.pubkey)
|
||||
pubkeys.append(proto.HDNodePathType(node=hd_node, address_n=tools.parse_path('m' + descriptor.path_suffix[i])))
|
||||
multisig = proto.MultisigRedeemScriptType(m=int(descriptor.multisig_M), signatures=[b''] * int(descriptor.multisig_N), pubkeys=pubkeys)
|
||||
# redeem_script means p2sh/multisig
|
||||
if redeem_script:
|
||||
elif redeem_script:
|
||||
# Get multisig object required by Trezor's get_address
|
||||
multisig = parse_multisig(bytes.fromhex(redeem_script))
|
||||
if not multisig[0]:
|
||||
@ -451,7 +460,7 @@ class TrezorClient(HardwareWalletClient):
|
||||
multisig=multisig,
|
||||
)
|
||||
return {'address': address}
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raise BadArgumentError("No path supplied matched device keys")
|
||||
@ -534,7 +543,7 @@ class TrezorClient(HardwareWalletClient):
|
||||
self._check_unlocked()
|
||||
try:
|
||||
device.apply_settings(self.client, use_passphrase=not self.client.features.passphrase_protection)
|
||||
except:
|
||||
except Exception:
|
||||
if self.type == 'Keepkey':
|
||||
print('Confirm the action by entering your PIN', file=sys.stderr)
|
||||
print('Use \'sendpin\' to provide the number positions for the PIN as displayed on your device\'s screen', file=sys.stderr)
|
||||
|
||||
@ -15,9 +15,10 @@
|
||||
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||
|
||||
import binascii
|
||||
from typing import Union
|
||||
|
||||
from . import messages
|
||||
from .tools import CallException, expect, normalize_nfc, session
|
||||
from .tools import expect, normalize_nfc, session
|
||||
|
||||
|
||||
@expect(messages.PublicKey)
|
||||
@ -62,7 +63,11 @@ def get_address(
|
||||
|
||||
@expect(messages.MessageSignature)
|
||||
def sign_message(
|
||||
client, coin_name, n, message, script_type=messages.InputScriptType.SPENDADDRESS
|
||||
client,
|
||||
coin_name,
|
||||
n,
|
||||
message: Union[str, bytes],
|
||||
script_type=messages.InputScriptType.SPENDADDRESS,
|
||||
):
|
||||
message = normalize_nfc(message)
|
||||
return client.call(
|
||||
@ -71,6 +76,7 @@ def sign_message(
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@session
|
||||
def sign_tx(client, coin_name, inputs, outputs, details=None, prev_txes=None):
|
||||
this_tx = messages.TransactionType(inputs=inputs, outputs=outputs)
|
||||
|
||||
@ -72,7 +72,7 @@ class TrezorClient:
|
||||
"""
|
||||
|
||||
def __init__(self, transport, ui=None, state=None):
|
||||
LOG.info("creating client instance for device: {}".format(transport.get_path()))
|
||||
LOG.debug("creating client instance for device: {}".format(transport.get_path()))
|
||||
self.transport = transport
|
||||
self.ui = ui
|
||||
self.state = state
|
||||
@ -149,7 +149,7 @@ class TrezorClient:
|
||||
|
||||
try:
|
||||
passphrase = self.ui.get_passphrase()
|
||||
except:
|
||||
except Exception:
|
||||
self.call_raw(messages.Cancel())
|
||||
raise
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
# You should have received a copy of the License along with this library.
|
||||
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import warnings
|
||||
@ -25,6 +26,8 @@ from .transport import enumerate_devices, get_transport
|
||||
|
||||
RECOVERY_BACK = "\x08" # backspace character, sent literally
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TrezorDevice:
|
||||
"""
|
||||
@ -192,7 +195,7 @@ def reset(
|
||||
raise RuntimeError("Invalid response, expected EntropyRequest")
|
||||
|
||||
external_entropy = os.urandom(32)
|
||||
# LOG.debug("Computer generated entropy: " + external_entropy.hex())
|
||||
LOG.debug("Computer generated entropy: " + external_entropy.hex())
|
||||
ret = client.call(proto.EntropyAck(entropy=external_entropy))
|
||||
client.init_device()
|
||||
return ret
|
||||
|
||||
@ -158,7 +158,7 @@ class MessageType:
|
||||
|
||||
def _fill_missing(self):
|
||||
# fill missing fields
|
||||
for fname, ftype, fflags in self.get_fields().values():
|
||||
for fname, _, fflags in self.get_fields().values():
|
||||
if not hasattr(self, fname):
|
||||
if fflags & FLAG_REPEATED:
|
||||
setattr(self, fname, [])
|
||||
|
||||
@ -19,7 +19,7 @@ import hashlib
|
||||
import re
|
||||
import struct
|
||||
import unicodedata
|
||||
from typing import List, NewType
|
||||
from typing import List, NewType, Union
|
||||
|
||||
from .exceptions import TrezorFailure
|
||||
|
||||
@ -181,13 +181,13 @@ def parse_path(nstr: str) -> Address:
|
||||
raise ValueError("Invalid BIP32 path", nstr)
|
||||
|
||||
|
||||
def normalize_nfc(txt):
|
||||
def normalize_nfc(txt: Union[str, bytes]) -> bytes:
|
||||
"""
|
||||
Normalize message to NFC and return bytes suitable for protobuf.
|
||||
This seems to be bitcoin-qt standard of doing things.
|
||||
"""
|
||||
if isinstance(txt, bytes):
|
||||
txt = txt.decode()
|
||||
return txt
|
||||
return unicodedata.normalize("NFC", txt).encode()
|
||||
|
||||
|
||||
|
||||
@ -121,7 +121,7 @@ def enumerate_devices() -> Iterable[Transport]:
|
||||
name = transport.__name__
|
||||
try:
|
||||
found = list(transport.enumerate())
|
||||
LOG.info("Enumerating {}: found {} devices".format(name, len(found)))
|
||||
LOG.debug("Enumerating {}: found {} devices".format(name, len(found)))
|
||||
devices.extend(found)
|
||||
except NotImplementedError:
|
||||
LOG.error("{} does not implement device enumeration".format(name))
|
||||
@ -144,7 +144,7 @@ def get_transport(path: str = None, prefix_search: bool = False) -> Transport:
|
||||
def match_prefix(a: str, b: str) -> bool:
|
||||
return a.startswith(b) or b.startswith(a)
|
||||
|
||||
LOG.info(
|
||||
LOG.debug(
|
||||
"looking for device by {}: {}".format(
|
||||
"prefix" if prefix_search else "full path", path
|
||||
)
|
||||
|
||||
@ -27,7 +27,7 @@ LOG = logging.getLogger(__name__)
|
||||
try:
|
||||
import hid
|
||||
except Exception as e:
|
||||
LOG.info("HID transport is disabled: {}".format(e))
|
||||
LOG.warning("HID transport is disabled: {}".format(e))
|
||||
hid = None
|
||||
|
||||
|
||||
|
||||
@ -129,7 +129,7 @@ class WebUsbTransport(ProtocolBasedTransport):
|
||||
# non-functional.
|
||||
dev.getProduct()
|
||||
devices.append(WebUsbTransport(dev))
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
return devices
|
||||
|
||||
|
||||
@ -3,12 +3,15 @@
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
from . import commands, __version__
|
||||
from .cli import HWIArgumentParser
|
||||
from .errors import handle_errors, DEVICE_NOT_INITIALIZED
|
||||
|
||||
try:
|
||||
from .ui.ui_bitbox02pairing import Ui_BitBox02PairingDialog
|
||||
from .ui.ui_displayaddressdialog import Ui_DisplayAddressDialog
|
||||
from .ui.ui_getxpubdialog import Ui_GetXpubDialog
|
||||
from .ui.ui_getkeypooloptionsdialog import Ui_GetKeypoolOptionsDialog
|
||||
@ -23,7 +26,9 @@ except ImportError:
|
||||
|
||||
from PySide2.QtGui import QRegExpValidator
|
||||
from PySide2.QtWidgets import QApplication, QDialog, QDialogButtonBox, QLineEdit, QMessageBox, QMainWindow
|
||||
from PySide2.QtCore import QRegExp, Signal, Slot
|
||||
from PySide2.QtCore import QCoreApplication, QRegExp, Signal, Slot
|
||||
|
||||
import bitbox02.util
|
||||
|
||||
def do_command(f, *args, **kwargs):
|
||||
result = {}
|
||||
@ -207,6 +212,50 @@ class GetKeypoolOptionsDialog(QDialog):
|
||||
self.ui.path_lineedit.setEnabled(True)
|
||||
self.ui.account_spinbox.setEnabled(False)
|
||||
|
||||
class BitBox02PairingDialog(QDialog):
|
||||
def __init__(self, pairing_code: str, device_response: Callable[[], bool]):
|
||||
super(BitBox02PairingDialog, self).__init__()
|
||||
self.ui = Ui_BitBox02PairingDialog()
|
||||
self.ui.setupUi(self)
|
||||
self.setWindowTitle('Verify BitBox02 pairing code')
|
||||
self.ui.pairingCode.setText(pairing_code.replace("\n", "<br>"))
|
||||
self.ui.buttonBox.setEnabled(False)
|
||||
self.device_response = device_response
|
||||
self.painted = False
|
||||
|
||||
def paintEvent(self, ev):
|
||||
super().paintEvent(ev)
|
||||
self.painted = True
|
||||
|
||||
def enable_buttons(self):
|
||||
self.ui.buttonBox.setEnabled(True)
|
||||
|
||||
class BitBox02NoiseConfig(bitbox02.util.BitBoxAppNoiseConfig):
|
||||
""" GUI elements to perform the BitBox02 pairing and attestatoin check """
|
||||
|
||||
def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool:
|
||||
dialog = BitBox02PairingDialog(code, device_response)
|
||||
dialog.show()
|
||||
# render the window since the next operation is blocking
|
||||
while True:
|
||||
QCoreApplication.processEvents()
|
||||
if dialog.painted:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
if not device_response():
|
||||
return False
|
||||
dialog.enable_buttons()
|
||||
dialog.exec_()
|
||||
return dialog.result() == QDialog.Accepted
|
||||
|
||||
def attestation_check(self, result: bool) -> None:
|
||||
if not result:
|
||||
QMessageBox.warning(
|
||||
None,
|
||||
"BitBox02 attestation check",
|
||||
"BitBox02 attestation check failed. Your BitBox02 might not be genuine. Please contact support@shiftcrypto.ch if the problem persists.",
|
||||
)
|
||||
|
||||
class HWIQt(QMainWindow):
|
||||
def __init__(self, passphrase='', testnet=False):
|
||||
super(HWIQt, self).__init__()
|
||||
@ -293,7 +342,6 @@ class HWIQt(QMainWindow):
|
||||
|
||||
self.ui.getxpub_button.setEnabled(True)
|
||||
self.ui.signtx_button.setEnabled(True)
|
||||
self.ui.signmsg_button.setEnabled(True)
|
||||
self.ui.display_addr_button.setEnabled(True)
|
||||
self.ui.getkeypool_opts_button.setEnabled(True)
|
||||
|
||||
@ -302,7 +350,12 @@ class HWIQt(QMainWindow):
|
||||
self.client = commands.get_client(self.device_info['model'], self.device_info['path'], self.passphrase)
|
||||
self.client.is_testnet = self.testnet
|
||||
|
||||
self.ui.toggle_passphrase_button.setEnabled(self.device_info['type'] == 'trezor' or self.device_info['type'] == 'keepkey')
|
||||
if self.device_info['type'] == 'bitbox02':
|
||||
self.client.set_noise_config(BitBox02NoiseConfig())
|
||||
|
||||
self.ui.setpass_button.setEnabled(self.device_info['type'] != 'bitbox02')
|
||||
self.ui.signmsg_button.setEnabled(self.device_info['type'] != 'bitbox02')
|
||||
self.ui.toggle_passphrase_button.setEnabled(self.device_info['type'] in ('trezor', 'keepkey', 'bitbox02', ))
|
||||
|
||||
self.get_device_info()
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from typing import Dict, Optional, Union
|
||||
|
||||
from .base58 import get_xpub_fingerprint_hex
|
||||
from .descriptor import Descriptor
|
||||
from .serializations import PSBT
|
||||
|
||||
|
||||
@ -55,10 +56,14 @@ class HardwareWalletClient(object):
|
||||
raise NotImplementedError("The HardwareWalletClient base class "
|
||||
"does not implement this method")
|
||||
|
||||
def sign_message(self, message: str, bip32_path: str) -> Dict[str, str]:
|
||||
def sign_message(
|
||||
self, message: Union[str, bytes], bip32_path: str
|
||||
) -> Dict[str, str]:
|
||||
"""Sign a message (bitcoin message signing).
|
||||
|
||||
Sign the message according to the bitcoin message signing standard.
|
||||
Sign the message according to the bitcoin message signing standard:
|
||||
usually, the message is a string that is encoded to bytes;
|
||||
anyway, if the message is already bytes it is processed untouched.
|
||||
|
||||
Retrieve the signing key at the specified BIP32 derivation path.
|
||||
|
||||
@ -73,6 +78,7 @@ class HardwareWalletClient(object):
|
||||
p2sh_p2wpkh: bool,
|
||||
bech32: bool,
|
||||
redeem_script: Optional[str] = None,
|
||||
descriptor: Optional[Descriptor] = None,
|
||||
) -> Dict[str, str]:
|
||||
"""Display and return the address of specified type.
|
||||
|
||||
@ -156,7 +162,7 @@ class HardwareWalletClient(object):
|
||||
raise NotImplementedError("The HardwareWalletClient base class "
|
||||
"does not implement this method")
|
||||
|
||||
def send_pin(self) -> Dict[str, Union[bool, str, int]]:
|
||||
def send_pin(self, pin: str) -> Dict[str, Union[bool, str, int]]:
|
||||
"""Send PIN.
|
||||
|
||||
Must return a dictionary with the "success" key,
|
||||
|
||||
0
hwilib/py.typed
Normal file
0
hwilib/py.typed
Normal file
@ -103,7 +103,7 @@ def deser_uint256(f: Readable) -> int:
|
||||
|
||||
def ser_uint256(u: int) -> bytes:
|
||||
rs = b""
|
||||
for i in range(8):
|
||||
for _ in range(8):
|
||||
rs += struct.pack("<I", u & 0xFFFFFFFF)
|
||||
u >>= 32
|
||||
return rs
|
||||
@ -121,7 +121,7 @@ D = TypeVar("D", bound=Deserializable)
|
||||
def deser_vector(f: Readable, c: Callable[[], D]) -> List[D]:
|
||||
nit = deser_compact_size(f)
|
||||
r = []
|
||||
for i in range(nit):
|
||||
for _ in range(nit):
|
||||
t = c()
|
||||
t.deserialize(f)
|
||||
r.append(t)
|
||||
@ -138,7 +138,7 @@ def ser_vector(v: Sequence[Serializable]) -> bytes:
|
||||
def deser_string_vector(f: Readable) -> List[bytes]:
|
||||
nit = deser_compact_size(f)
|
||||
r = []
|
||||
for i in range(nit):
|
||||
for _ in range(nit):
|
||||
t = deser_string(f)
|
||||
r.append(t)
|
||||
return r
|
||||
@ -456,7 +456,7 @@ class CTransaction(object):
|
||||
if (len(self.wit.vtxinwit) != len(self.vin)):
|
||||
# vtxinwit must have the same length as vin
|
||||
self.wit.vtxinwit = self.wit.vtxinwit[:len(self.vin)]
|
||||
for i in range(len(self.wit.vtxinwit), len(self.vin)):
|
||||
for _ in range(len(self.wit.vtxinwit), len(self.vin)):
|
||||
self.wit.vtxinwit.append(CTxInWitness())
|
||||
r += self.wit.serialize()
|
||||
r += struct.pack("<I", self.nLockTime)
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1b7c", MODE="0660", GROUP="plugdev"
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="2b7c", MODE="0660", GROUP="plugdev"
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="3b7c", MODE="0660", GROUP="plugdev"
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="4b7c", MODE="0660", GROUP="plugdev"
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1807", MODE="0660", GROUP="plugdev"
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1808", MODE="0660", GROUP="plugdev"
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0000", MODE="0660", GROUP="plugdev"
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0001", MODE="0660", GROUP="plugdev"
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0004", MODE="0660", GROUP="plugdev"
|
||||
# HW.1 / Nano
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1b7c|2b7c|3b7c|4b7c", TAG+="uaccess", TAG+="udev-acl"
|
||||
# Blue
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0000|0000|0001|0002|0003|0004|0005|0006|0007|0008|0009|000a|000b|000c|000d|000e|000f|0010|0011|0012|0013|0014|0015|0016|0017|0018|0019|001a|001b|001c|001d|001e|001f", TAG+="uaccess", TAG+="udev-acl"
|
||||
# Nano S
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0001|1000|1001|1002|1003|1004|1005|1006|1007|1008|1009|100a|100b|100c|100d|100e|100f|1010|1011|1012|1013|1014|1015|1016|1017|1018|1019|101a|101b|101c|101d|101e|101f", TAG+="uaccess", TAG+="udev-acl"
|
||||
# Aramis
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0002|2000|2001|2002|2003|2004|2005|2006|2007|2008|2009|200a|200b|200c|200d|200e|200f|2010|2011|2012|2013|2014|2015|2016|2017|2018|2019|201a|201b|201c|201d|201e|201f", TAG+="uaccess", TAG+="udev-acl"
|
||||
# HW2
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0003|3000|3001|3002|3003|3004|3005|3006|3007|3008|3009|300a|300b|300c|300d|300e|300f|3010|3011|3012|3013|3014|3015|3016|3017|3018|3019|301a|301b|301c|301d|301e|301f", TAG+="uaccess", TAG+="udev-acl"
|
||||
# Nano X
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0004|4000|4001|4002|4003|4004|4005|4006|4007|4008|4009|400a|400b|400c|400d|400e|400f|4010|4011|4012|4013|4014|4015|4016|4017|4018|4019|401a|401b|401c|401d|401e|401f", TAG+="uaccess", TAG+="udev-acl"
|
||||
|
||||
1
hwilib/udev/53-hid-bitbox02.rules
Normal file
1
hwilib/udev/53-hid-bitbox02.rules
Normal file
@ -0,0 +1 @@
|
||||
SUBSYSTEM=="usb", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02_%n", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403"
|
||||
1
hwilib/udev/54-hid-bitbox02.rules
Normal file
1
hwilib/udev/54-hid-bitbox02.rules
Normal file
@ -0,0 +1 @@
|
||||
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02-%n"
|
||||
120
hwilib/ui/bitbox02pairing.ui
Normal file
120
hwilib/ui/bitbox02pairing.ui
Normal file
@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>BitBox02PairingDialog</class>
|
||||
<widget class="QDialog" name="BitBox02PairingDialog">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::WindowModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>209</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
</property>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>30</x>
|
||||
<y>160</y>
|
||||
<width>341</width>
|
||||
<height>32</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::No|QDialogButtonBox::Yes</set>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLabel" name="pairingCode">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>20</x>
|
||||
<y>80</y>
|
||||
<width>331</width>
|
||||
<height>61</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>DejaVu Sans Mono</family>
|
||||
<pointsize>15</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::RichText</enum>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>20</x>
|
||||
<y>10</y>
|
||||
<width>351</width>
|
||||
<height>61</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>11</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Please verify the pairing code matches what is
|
||||
shown on your BitBox02.</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>BitBox02PairingDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>BitBox02PairingDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
4
mypy.ini
Normal file
4
mypy.ini
Normal file
@ -0,0 +1,4 @@
|
||||
[mypy]
|
||||
# Do not type check trezorlib because it does not provide types.
|
||||
[mypy-hwilib.devices.trezorlib.*]
|
||||
follow_imports = skip
|
||||
200
poetry.lock
generated
200
poetry.lock
generated
@ -17,6 +17,62 @@ version = "1.5.2"
|
||||
[package.dependencies]
|
||||
pycodestyle = ">=2.5.0"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Base58 and Base58Check implementation"
|
||||
name = "base58"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "2.0.1"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Python library for bitbox02 communication"
|
||||
name = "bitbox02"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
version = "4.1.0"
|
||||
|
||||
[package.dependencies]
|
||||
base58 = ">=2.0.0"
|
||||
ecdsa = ">=0.13"
|
||||
hidapi = ">=0.7.99.post21"
|
||||
noiseprotocol = ">=0.3"
|
||||
protobuf = ">=3.7"
|
||||
semver = ">=2.8.1"
|
||||
typing-extensions = ">=3.7.4"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Foreign Function Interface for Python calling C code."
|
||||
name = "cffi"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.14.1"
|
||||
|
||||
[package.dependencies]
|
||||
pycparser = "*"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
name = "cryptography"
|
||||
optional = false
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
|
||||
version = "3.0"
|
||||
|
||||
[package.dependencies]
|
||||
cffi = ">=1.8,<1.11.3 || >1.11.3"
|
||||
six = ">=1.4.1"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0,<3.1.0 || >3.1.0,<3.1.1 || >3.1.1)", "sphinx-rtd-theme"]
|
||||
docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
|
||||
idna = ["idna (>=2.1)"]
|
||||
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Python 2.7 backport of the \"dis\" module from Python 3.5+"
|
||||
@ -125,6 +181,17 @@ version = "0.18"
|
||||
[package.dependencies]
|
||||
pbkdf2 = "*"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Implementation of Noise Protocol Framework"
|
||||
name = "noiseprotocol"
|
||||
optional = false
|
||||
python-versions = "~=3.5"
|
||||
version = "0.3.1"
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = ">=2.8"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "PKCS#5 v2.0 PBKDF2 Module"
|
||||
@ -145,6 +212,18 @@ version = "2019.4.18"
|
||||
[package.dependencies]
|
||||
future = "*"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Protocol Buffers"
|
||||
name = "protobuf"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "3.12.4"
|
||||
|
||||
[package.dependencies]
|
||||
setuptools = "*"
|
||||
six = ">=1.9"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Pure-Python Implementation of the AES block-cipher and common modes of operation"
|
||||
@ -161,6 +240,14 @@ optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "2.6.0"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "C parser in Python"
|
||||
name = "pycparser"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "2.20"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "passive checker of Python programs"
|
||||
@ -202,6 +289,14 @@ optional = false
|
||||
python-versions = "*"
|
||||
version = "0.2.0"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Python helper for Semantic Versioning (http://semver.org/)"
|
||||
name = "semver"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "2.10.2"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Python / C++ bindings helper module"
|
||||
@ -210,6 +305,14 @@ optional = true
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.9"
|
||||
version = "5.14.2.1"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
name = "six"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
version = "1.15.0"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Backported and Experimental Type Hints for Python 3.5+"
|
||||
@ -235,7 +338,8 @@ testing = ["jaraco.itertools", "func-timeout"]
|
||||
qt = ["pyside2"]
|
||||
|
||||
[metadata]
|
||||
content-hash = "bce2677f0c45cb74f03eecfab2094577eed8fb16fe305a21c924128c4faa1b47"
|
||||
content-hash = "fc39b2be42f870113feb38f56c177164fb7302eb07616d850f5a1b9e3e8509e1"
|
||||
lock-version = "1.0"
|
||||
python-versions = "^3.6,<3.9"
|
||||
|
||||
[metadata.files]
|
||||
@ -246,6 +350,65 @@ altgraph = [
|
||||
autopep8 = [
|
||||
{file = "autopep8-1.5.2.tar.gz", hash = "sha256:152fd8fe47d02082be86e05001ec23d6f420086db56b17fc883f3f965fb34954"},
|
||||
]
|
||||
base58 = [
|
||||
{file = "base58-2.0.1-py3-none-any.whl", hash = "sha256:447adc750d6b642987ffc6d397ecd15a799852d5f6a1d308d384500243825058"},
|
||||
{file = "base58-2.0.1.tar.gz", hash = "sha256:365c9561d9babac1b5f18ee797508cd54937a724b6e419a130abad69cec5ca79"},
|
||||
]
|
||||
bitbox02 = [
|
||||
{file = "bitbox02-4.1.0-py3-none-any.whl", hash = "sha256:1af95952d67b74c80ccc0588e0aee983c764960da637bd24bc41a1cb89d5e127"},
|
||||
{file = "bitbox02-4.1.0.tar.gz", hash = "sha256:73a35594162f32897dd2b1880f0cfaa42922acd1c2d7f4cf3d94b8333329c931"},
|
||||
]
|
||||
cffi = [
|
||||
{file = "cffi-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2"},
|
||||
{file = "cffi-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8"},
|
||||
{file = "cffi-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1"},
|
||||
{file = "cffi-1.14.1-cp27-cp27m-win32.whl", hash = "sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9"},
|
||||
{file = "cffi-1.14.1-cp27-cp27m-win_amd64.whl", hash = "sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1"},
|
||||
{file = "cffi-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168"},
|
||||
{file = "cffi-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf"},
|
||||
{file = "cffi-1.14.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e"},
|
||||
{file = "cffi-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849"},
|
||||
{file = "cffi-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c"},
|
||||
{file = "cffi-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa"},
|
||||
{file = "cffi-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948"},
|
||||
{file = "cffi-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f"},
|
||||
{file = "cffi-1.14.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3"},
|
||||
{file = "cffi-1.14.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc"},
|
||||
{file = "cffi-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2"},
|
||||
{file = "cffi-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022"},
|
||||
{file = "cffi-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9"},
|
||||
{file = "cffi-1.14.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0"},
|
||||
{file = "cffi-1.14.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33"},
|
||||
{file = "cffi-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792"},
|
||||
{file = "cffi-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96"},
|
||||
{file = "cffi-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc"},
|
||||
{file = "cffi-1.14.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939"},
|
||||
{file = "cffi-1.14.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe"},
|
||||
{file = "cffi-1.14.1-cp38-cp38-win32.whl", hash = "sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995"},
|
||||
{file = "cffi-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90"},
|
||||
{file = "cffi-1.14.1.tar.gz", hash = "sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f"},
|
||||
]
|
||||
cryptography = [
|
||||
{file = "cryptography-3.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ab49edd5bea8d8b39a44b3db618e4783ef84c19c8b47286bf05dfdb3efb01c83"},
|
||||
{file = "cryptography-3.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:124af7255ffc8e964d9ff26971b3a6153e1a8a220b9a685dc407976ecb27a06a"},
|
||||
{file = "cryptography-3.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:51e40123083d2f946794f9fe4adeeee2922b581fa3602128ce85ff813d85b81f"},
|
||||
{file = "cryptography-3.0-cp27-cp27m-win32.whl", hash = "sha256:dea0ba7fe6f9461d244679efa968d215ea1f989b9c1957d7f10c21e5c7c09ad6"},
|
||||
{file = "cryptography-3.0-cp27-cp27m-win_amd64.whl", hash = "sha256:8ecf9400d0893836ff41b6f977a33972145a855b6efeb605b49ee273c5e6469f"},
|
||||
{file = "cryptography-3.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c608ff4d4adad9e39b5057de43657515c7da1ccb1807c3a27d4cf31fc923b4b"},
|
||||
{file = "cryptography-3.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:bec7568c6970b865f2bcebbe84d547c52bb2abadf74cefce396ba07571109c67"},
|
||||
{file = "cryptography-3.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:0cbfed8ea74631fe4de00630f4bb592dad564d57f73150d6f6796a24e76c76cd"},
|
||||
{file = "cryptography-3.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:a09fd9c1cca9a46b6ad4bea0a1f86ab1de3c0c932364dbcf9a6c2a5eeb44fa77"},
|
||||
{file = "cryptography-3.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:ce82cc06588e5cbc2a7df3c8a9c778f2cb722f56835a23a68b5a7264726bb00c"},
|
||||
{file = "cryptography-3.0-cp35-cp35m-win32.whl", hash = "sha256:9367d00e14dee8d02134c6c9524bb4bd39d4c162456343d07191e2a0b5ec8b3b"},
|
||||
{file = "cryptography-3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:384d7c681b1ab904fff3400a6909261cae1d0939cc483a68bdedab282fb89a07"},
|
||||
{file = "cryptography-3.0-cp36-cp36m-win32.whl", hash = "sha256:4d355f2aee4a29063c10164b032d9fa8a82e2c30768737a2fd56d256146ad559"},
|
||||
{file = "cryptography-3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:45741f5499150593178fc98d2c1a9c6722df88b99c821ad6ae298eff0ba1ae71"},
|
||||
{file = "cryptography-3.0-cp37-cp37m-win32.whl", hash = "sha256:8ecef21ac982aa78309bb6f092d1677812927e8b5ef204a10c326fc29f1367e2"},
|
||||
{file = "cryptography-3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4b9303507254ccb1181d1803a2080a798910ba89b1a3c9f53639885c90f7a756"},
|
||||
{file = "cryptography-3.0-cp38-cp38-win32.whl", hash = "sha256:8713ddb888119b0d2a1462357d5946b8911be01ddbf31451e1d07eaa5077a261"},
|
||||
{file = "cryptography-3.0-cp38-cp38-win_amd64.whl", hash = "sha256:bea0b0468f89cdea625bb3f692cd7a4222d80a6bdafd6fb923963f2b9da0e15f"},
|
||||
{file = "cryptography-3.0.tar.gz", hash = "sha256:8e924dbc025206e97756e8903039662aa58aa9ba357d8e1d8fc29e3092322053"},
|
||||
]
|
||||
dis3 = [
|
||||
{file = "dis3-0.1.3-py2-none-any.whl", hash = "sha256:61f7720dd0d8749d23fda3d7227ce74d73da11c2fade993a67ab2f9852451b14"},
|
||||
{file = "dis3-0.1.3-py3-none-any.whl", hash = "sha256:30b6412d33d738663e8ded781b138f4b01116437f0872aa56aa3adba6aeff218"},
|
||||
@ -293,12 +456,35 @@ mccabe = [
|
||||
mnemonic = [
|
||||
{file = "mnemonic-0.18.tar.gz", hash = "sha256:02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d"},
|
||||
]
|
||||
noiseprotocol = [
|
||||
{file = "noiseprotocol-0.3.1-py3-none-any.whl", hash = "sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111"},
|
||||
]
|
||||
pbkdf2 = [
|
||||
{file = "pbkdf2-1.3.tar.gz", hash = "sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979"},
|
||||
]
|
||||
pefile = [
|
||||
{file = "pefile-2019.4.18.tar.gz", hash = "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"},
|
||||
]
|
||||
protobuf = [
|
||||
{file = "protobuf-3.12.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3d59825cba9447e8f4fcacc1f3c892cafd28b964e152629b3f420a2fb5918b5a"},
|
||||
{file = "protobuf-3.12.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6009f3ebe761fad319b52199a49f1efa7a3729302947a78a3f5ea8e7e89e3ac2"},
|
||||
{file = "protobuf-3.12.4-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:e2bd5c98952db3f1bb1af2e81b6a208909d3b8a2d32f7525c5cc10a6338b6593"},
|
||||
{file = "protobuf-3.12.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2becd0e238ae34caf96fa7365b87f65b88aebcf7864dfe5ab461c5005f4256d9"},
|
||||
{file = "protobuf-3.12.4-cp35-cp35m-win32.whl", hash = "sha256:ef991cbe34d7bb935ba6349406a210d3558b9379c21621c6ed7b99112af7350e"},
|
||||
{file = "protobuf-3.12.4-cp35-cp35m-win_amd64.whl", hash = "sha256:a7b6cf201e67132ca99b8a6c4812fab541fdce1ceb54bb6f66bc336ab7259138"},
|
||||
{file = "protobuf-3.12.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4794a7748ee645d2ae305f3f4f0abd459e789c973b5bc338008960f83e0c554b"},
|
||||
{file = "protobuf-3.12.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f1796e0eb911bf5b08e76b753953effbeb6bc42c95c16597177f627eaa52c375"},
|
||||
{file = "protobuf-3.12.4-cp36-cp36m-win32.whl", hash = "sha256:c0c8d7c8f07eacd9e98a907941b56e57883cf83de069cfaeaa7e02c582f72ddb"},
|
||||
{file = "protobuf-3.12.4-cp36-cp36m-win_amd64.whl", hash = "sha256:2db6940c1914fa3fbfabc0e7c8193d9e18b01dbb4650acac249b113be3ba8d9e"},
|
||||
{file = "protobuf-3.12.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6842284bb15f1b19c50c5fd496f1e2a4cfefdbdfa5d25c02620cb82793295a7"},
|
||||
{file = "protobuf-3.12.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0b00429b87821f1e6f3d641327864e6f271763ae61799f7540bc58a352825fe2"},
|
||||
{file = "protobuf-3.12.4-cp37-cp37m-win32.whl", hash = "sha256:f10ba89f9cd508dc00e469918552925ef7cba38d101ca47af1e78f2f9982c6b3"},
|
||||
{file = "protobuf-3.12.4-cp37-cp37m-win_amd64.whl", hash = "sha256:2636c689a6a2441da9a2ef922a21f9b8bfd5dfe676abd77d788db4b36ea86bee"},
|
||||
{file = "protobuf-3.12.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:50b7bb2124f6a1fb0ddc6a44428ae3a21e619ad2cdf08130ac6c00534998ef07"},
|
||||
{file = "protobuf-3.12.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e77ca4e1403b363a88bde9e31c11d093565e925e1685f40b29385a52f2320794"},
|
||||
{file = "protobuf-3.12.4-py2.py3-none-any.whl", hash = "sha256:32f0bcdf85e0040f36b4f548c71177027f2a618cab00ba235197fa9e230b7289"},
|
||||
{file = "protobuf-3.12.4.tar.gz", hash = "sha256:c99e5aea75b6f2b29c8d8da5bdc5f5ed8d9a5b4f15115c8316a3f0a850f94656"},
|
||||
]
|
||||
pyaes = [
|
||||
{file = "pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"},
|
||||
]
|
||||
@ -306,6 +492,10 @@ pycodestyle = [
|
||||
{file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
|
||||
{file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"},
|
||||
]
|
||||
pycparser = [
|
||||
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
|
||||
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
|
||||
]
|
||||
pyflakes = [
|
||||
{file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"},
|
||||
{file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
|
||||
@ -325,6 +515,10 @@ pywin32-ctypes = [
|
||||
{file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"},
|
||||
{file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"},
|
||||
]
|
||||
semver = [
|
||||
{file = "semver-2.10.2-py2.py3-none-any.whl", hash = "sha256:21e80ca738975ed513cba859db0a0d2faca2380aef1962f48272ebf9a8a44bd4"},
|
||||
{file = "semver-2.10.2.tar.gz", hash = "sha256:c0a4a9d1e45557297a722ee9bac3de2ec2ea79016b6ffcaca609b0bc62cf4276"},
|
||||
]
|
||||
shiboken2 = [
|
||||
{file = "shiboken2-5.14.2.1-5.14.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:d285d476a76f254bff69cc58c1d4385df295b42de1a818d4a8d11694c2d728fc"},
|
||||
{file = "shiboken2-5.14.2.1-5.14.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:73d03e74f542204e351539e42ab3e3727a69408e1497af4c6e84fb66c3e706d8"},
|
||||
@ -333,6 +527,10 @@ shiboken2 = [
|
||||
{file = "shiboken2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:fe4d0cf6737f1d01944be4cf3b401d74015c515ab84622bf04f47d64ffcd39f9"},
|
||||
{file = "shiboken2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:c022203b7cf01df6ad0bb190d286c2965958243a16e47bee8c5e6bbb9d0cd475"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
|
||||
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"},
|
||||
{file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "hwi"
|
||||
version = "1.1.2"
|
||||
version = "1.2.1"
|
||||
description = "A library for working with Bitcoin hardware wallets"
|
||||
authors = ["Andrew Chow <andrew@achow101.com>"]
|
||||
license = "MIT"
|
||||
@ -8,7 +8,7 @@ readme = "README.md"
|
||||
repository = "https://github.com/bitcoin-core/HWI"
|
||||
homepage = "https://github.com/bitcoin-core/HWI"
|
||||
exclude = ["docs/", "test/"]
|
||||
include = ["hwilib/**/*.py", "udev/"]
|
||||
include = ["hwilib/**/*.py", "udev/", "hwilib/py.typed"]
|
||||
packages = [
|
||||
{ include = "hwi.py" },
|
||||
{ include = "hwi-qt.py" },
|
||||
@ -24,6 +24,7 @@ mnemonic = "^0.18.0"
|
||||
typing-extensions = "^3.7"
|
||||
libusb1 = "^1.7"
|
||||
pyside2 = { version = "^5.14.0", optional = true }
|
||||
bitbox02 = ">=4.1.0"
|
||||
|
||||
[tool.poetry.extras]
|
||||
qt = ["pyside2"]
|
||||
|
||||
26
setup.py
26
setup.py
@ -13,6 +13,17 @@ packages = \
|
||||
package_data = \
|
||||
{'': ['*'],
|
||||
'hwilib': ['udev/*',
|
||||
'ui/bitbox02pairing.ui',
|
||||
'ui/bitbox02pairing.ui',
|
||||
'ui/bitbox02pairing.ui',
|
||||
'ui/bitbox02pairing.ui',
|
||||
'ui/bitbox02pairing.ui',
|
||||
'ui/bitbox02pairing.ui',
|
||||
'ui/bitbox02pairing.ui',
|
||||
'ui/bitbox02pairing.ui',
|
||||
'ui/bitbox02pairing.ui',
|
||||
'ui/bitbox02pairing.ui',
|
||||
'ui/displayaddressdialog.ui',
|
||||
'ui/displayaddressdialog.ui',
|
||||
'ui/displayaddressdialog.ui',
|
||||
'ui/displayaddressdialog.ui',
|
||||
@ -31,6 +42,8 @@ package_data = \
|
||||
'ui/getkeypooloptionsdialog.ui',
|
||||
'ui/getkeypooloptionsdialog.ui',
|
||||
'ui/getkeypooloptionsdialog.ui',
|
||||
'ui/getkeypooloptionsdialog.ui',
|
||||
'ui/getxpubdialog.ui',
|
||||
'ui/getxpubdialog.ui',
|
||||
'ui/getxpubdialog.ui',
|
||||
'ui/getxpubdialog.ui',
|
||||
@ -49,6 +62,8 @@ package_data = \
|
||||
'ui/hwiqt.pyproject',
|
||||
'ui/hwiqt.pyproject',
|
||||
'ui/hwiqt.pyproject',
|
||||
'ui/hwiqt.pyproject',
|
||||
'ui/mainwindow.ui',
|
||||
'ui/mainwindow.ui',
|
||||
'ui/mainwindow.ui',
|
||||
'ui/mainwindow.ui',
|
||||
@ -67,6 +82,8 @@ package_data = \
|
||||
'ui/sendpindialog.ui',
|
||||
'ui/sendpindialog.ui',
|
||||
'ui/sendpindialog.ui',
|
||||
'ui/sendpindialog.ui',
|
||||
'ui/setpassphrasedialog.ui',
|
||||
'ui/setpassphrasedialog.ui',
|
||||
'ui/setpassphrasedialog.ui',
|
||||
'ui/setpassphrasedialog.ui',
|
||||
@ -85,6 +102,8 @@ package_data = \
|
||||
'ui/signmessagedialog.ui',
|
||||
'ui/signmessagedialog.ui',
|
||||
'ui/signmessagedialog.ui',
|
||||
'ui/signmessagedialog.ui',
|
||||
'ui/signpsbtdialog.ui',
|
||||
'ui/signpsbtdialog.ui',
|
||||
'ui/signpsbtdialog.ui',
|
||||
'ui/signpsbtdialog.ui',
|
||||
@ -98,7 +117,8 @@ package_data = \
|
||||
modules = \
|
||||
['hwi', 'hwi-qt']
|
||||
install_requires = \
|
||||
['ecdsa>=0.13.0,<0.14.0',
|
||||
['bitbox02>=4.1.0',
|
||||
'ecdsa>=0.13.0,<0.14.0',
|
||||
'hidapi>=0.7.99,<0.8.0',
|
||||
'libusb1>=1.7,<2.0',
|
||||
'mnemonic>=0.18.0,<0.19.0',
|
||||
@ -113,9 +133,9 @@ entry_points = \
|
||||
|
||||
setup_kwargs = {
|
||||
'name': 'hwi',
|
||||
'version': '1.1.2',
|
||||
'version': '1.2.1',
|
||||
'description': 'A library for working with Bitcoin hardware wallets',
|
||||
'long_description': "# Bitcoin Hardware Wallet Interface\n\n[](https://travis-ci.org/bitcoin-core/HWI)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependecies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path is known, issue commands to it like so:\n\n```\n./hwi.py -t <type> -d <path> <command> <command args>\n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | Digital BitBox | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | N/A |\n| Device Backup | N/A | N/A | N/A | N/A | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | N/A | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n",
|
||||
'long_description': "# Bitcoin Hardware Wallet Interface\n\n[](https://travis-ci.org/bitcoin-core/HWI)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\nCaveat emptor: Inclusion of a specific hardware wallet vendor does not imply any endorsement of quality or security.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependecies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path is known, issue commands to it like so:\n\n```\n./hwi.py -t <type> -d <path> <command> <command args>\n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | BitBox01 | BitBox02 | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A |\n| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A |\n| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | Yes | N/A |\n| Device Backup | N/A | N/A | N/A | N/A | Yes | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |\n| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |\n| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | Yes | Yes | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n",
|
||||
'author': 'Andrew Chow',
|
||||
'author_email': 'andrew@achow101.com',
|
||||
'maintainer': None,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
From cadbd3d25306b43060fd06eed589947d537a5ced Mon Sep 17 00:00:00 2001
|
||||
From 8793fbaa9b32f3c67f289a05194e68acc5c61b7d Mon Sep 17 00:00:00 2001
|
||||
From: Andrew Chow <achow101-github@achow101.com>
|
||||
Date: Tue, 17 Dec 2019 17:56:05 -0500
|
||||
Subject: [PATCH 2/2] Change default simulator multisig
|
||||
@ -8,7 +8,7 @@ Subject: [PATCH 2/2] Change default simulator multisig
|
||||
1 file changed, 5 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/unix/frozen-modules/sim_settings.py b/unix/frozen-modules/sim_settings.py
|
||||
index 0313c3e..e2c3d71 100644
|
||||
index 0313c3e..6d301d4 100644
|
||||
--- a/unix/frozen-modules/sim_settings.py
|
||||
+++ b/unix/frozen-modules/sim_settings.py
|
||||
@@ -68,7 +68,11 @@ if '--ms' in sys.argv:
|
||||
@ -17,13 +17,13 @@ index 0313c3e..e2c3d71 100644
|
||||
# P2SH: 2of4 using BIP39 passwords: "Me", "Myself", "and I", and (empty string) on simulator
|
||||
- sim_defaults['multisig'] = [['MeMyself', [2, 4], [[3503269483, 'tpubD9429UXFGCTKJ9NdiNK4rC5ygqSUkginycYHccqSg5gkmyQ7PZRHNjk99M6a6Y3NY8ctEUUJvCu6iCCui8Ju3xrHRu3Ez1CKB4ZFoRZDdP9'], [2389277556, 'tpubD97nVL37v5tWyMf9ofh5rznwhh1593WMRg6FT4o6MRJkKWANtwAMHYLrcJFsFmPfYbY1TE1LLQ4KBb84LBPt1ubvFwoosvMkcWJtMwvXgSc'], [3190206587, 'tpubD9ArfXowvGHnuECKdGXVKDMfZVGdephVWg8fWGWStH3VKHzT4ph3A4ZcgXWqFu1F5xGTfxncmrnf3sLC86dup2a8Kx7z3xQ3AgeNTQeFxPa'], [1130956047, 'tpubD8NXmKsmWp3a3DXhbihAYbYLGaRNVdTnr6JoSxxfXYQcmwVtW2hv8QoDwng6JtEonmJoL3cNEwfd2cLXMpGezwZ2vL2dQ7259bueNKj9C8n']], {'ch': 'XTN', 'pp': "45'"}]]
|
||||
+ sim_defaults['multisig'] = [
|
||||
+ ['mstest', [2, 3], [[1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj']], {'ft': 8, 'ch': 'XTN'}],
|
||||
+ ['mstest1', [2, 3], [[1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj']], {'ft': 14, 'ch': 'XTN'}],
|
||||
+ ['mstest2', [2, 3], [[1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj']], {'ft': 26, 'ch': 'XTN'}],
|
||||
+ ['mstest', [2, 3], [[1130956047, 'tpubDCp1a2CuSdeVLYbjKRF6H1oSU2hubMm6oV4tXYFkh5u7BwhJ1P5ZwntWGfNCx92BUWpbYPcbgApbYHNTEED49vFdscWgg6KpJepKdgBB92U'], [1130956047, 'tpubDCp1a2CuSdeVQxVNMuLWW4GDnSYqzaPwYRfb8uveDQmwYaPBXERNPWUR8GvyfSLDsbr9MwQxLsKeAAjSsigKpmgrkLdHJM77C3us7t5QFL2'], [1130956047, 'tpubDCp1a2CuSdeVS6h1LsXGqpALo6ZhvWEFDYDv8qTgcnBZYdAPsZ7QL25UKdUqKsMcc8eMQyZA9zjvUMaJjdSVcfDftzgmdvJfH5MnrZoxzFG']], {'ft': 8, 'ch': 'XTN'}],
|
||||
+ ['mstest1', [2, 3], [[1130956047, 'tpubDCp1a2CuSdeVLYbjKRF6H1oSU2hubMm6oV4tXYFkh5u7BwhJ1P5ZwntWGfNCx92BUWpbYPcbgApbYHNTEED49vFdscWgg6KpJepKdgBB92U'], [1130956047, 'tpubDCp1a2CuSdeVQxVNMuLWW4GDnSYqzaPwYRfb8uveDQmwYaPBXERNPWUR8GvyfSLDsbr9MwQxLsKeAAjSsigKpmgrkLdHJM77C3us7t5QFL2'], [1130956047, 'tpubDCp1a2CuSdeVS6h1LsXGqpALo6ZhvWEFDYDv8qTgcnBZYdAPsZ7QL25UKdUqKsMcc8eMQyZA9zjvUMaJjdSVcfDftzgmdvJfH5MnrZoxzFG']], {'ft': 14, 'ch': 'XTN'}],
|
||||
+ ['mstest2', [2, 3], [[1130956047, 'tpubDCp1a2CuSdeVLYbjKRF6H1oSU2hubMm6oV4tXYFkh5u7BwhJ1P5ZwntWGfNCx92BUWpbYPcbgApbYHNTEED49vFdscWgg6KpJepKdgBB92U'], [1130956047, 'tpubDCp1a2CuSdeVQxVNMuLWW4GDnSYqzaPwYRfb8uveDQmwYaPBXERNPWUR8GvyfSLDsbr9MwQxLsKeAAjSsigKpmgrkLdHJM77C3us7t5QFL2'], [1130956047, 'tpubDCp1a2CuSdeVS6h1LsXGqpALo6ZhvWEFDYDv8qTgcnBZYdAPsZ7QL25UKdUqKsMcc8eMQyZA9zjvUMaJjdSVcfDftzgmdvJfH5MnrZoxzFG']], {'ft': 26, 'ch': 'XTN'}],
|
||||
+ ]
|
||||
sim_defaults['fee_limit'] = -1
|
||||
|
||||
if '--xfp' in sys.argv:
|
||||
--
|
||||
2.27.0
|
||||
2.28.0
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ def coldcard_test_suite(simulator, rpc, userpass, interface):
|
||||
break
|
||||
if found:
|
||||
break
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
# Cleanup
|
||||
|
||||
@ -16,6 +16,18 @@ class TestDescriptor(unittest.TestCase):
|
||||
self.assertEqual(desc.testnet, True)
|
||||
self.assertEqual(desc.m_path, "m/84'/1'/0'/0/0")
|
||||
|
||||
def test_parse_multisig_descriptor_with_origin(self):
|
||||
desc = Descriptor.parse("wsh(multi(2,[00000001/48'/0'/0'/2']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48'/0'/0'/2']tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0))", True)
|
||||
self.assertIsNotNone(desc)
|
||||
self.assertEqual(desc.wsh, True)
|
||||
self.assertEqual(desc.origin_fingerprint, ["00000001", "00000002"])
|
||||
self.assertEqual(desc.origin_path, ["/48'/0'/0'/2'", "/48'/0'/0'/2'"])
|
||||
self.assertEqual(desc.base_key, ["tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B", "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty"])
|
||||
self.assertEqual(desc.path_suffix, ["/0/0", "/0/0"])
|
||||
self.assertEqual(desc.testnet, True)
|
||||
self.assertEqual(desc.m_path_base, ["m/48'/0'/0'/2'", "m/48'/0'/0'/2'"])
|
||||
self.assertEqual(desc.m_path, ["m/48'/0'/0'/2'/0/0", "m/48'/0'/0'/2'/0/0"])
|
||||
|
||||
def test_parse_descriptor_without_origin(self):
|
||||
desc = Descriptor.parse("wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)", True)
|
||||
self.assertIsNotNone(desc)
|
||||
@ -28,6 +40,18 @@ class TestDescriptor(unittest.TestCase):
|
||||
self.assertEqual(desc.testnet, True)
|
||||
self.assertEqual(desc.m_path, None)
|
||||
|
||||
def test_parse_descriptor_with_origin_fingerprint_only(self):
|
||||
desc = Descriptor.parse("wpkh([00000001]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)", True)
|
||||
self.assertIsNotNone(desc)
|
||||
self.assertEqual(desc.wpkh, True)
|
||||
self.assertEqual(desc.sh_wpkh, None)
|
||||
self.assertEqual(desc.origin_fingerprint, "00000001")
|
||||
self.assertEqual(desc.origin_path, "")
|
||||
self.assertEqual(desc.base_key, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B")
|
||||
self.assertEqual(desc.path_suffix, "/0/0")
|
||||
self.assertEqual(desc.testnet, True)
|
||||
self.assertEqual(desc.m_path, None)
|
||||
|
||||
def test_parse_descriptor_with_key_at_end_with_origin(self):
|
||||
desc = Descriptor.parse("wpkh([00000001/84'/1'/0'/0/0]0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)", True)
|
||||
self.assertIsNotNone(desc)
|
||||
|
||||
@ -13,8 +13,12 @@ import unittest
|
||||
from authproxy import AuthServiceProxy, JSONRPCException
|
||||
from hwilib.base58 import xpub_to_pub_hex
|
||||
from hwilib.cli import process_commands
|
||||
from hwilib.descriptor import AddChecksum
|
||||
from hwilib.serializations import PSBT
|
||||
|
||||
SUPPORTS_MS_DISPLAY = {'trezor_1', 'keepkey', 'coldcard', 'trezor_t'}
|
||||
SUPPORTS_XPUB_MS_DISPLAY = {'trezor_t'}
|
||||
|
||||
# Class for emulator control
|
||||
class DeviceEmulator():
|
||||
def start(self):
|
||||
@ -64,7 +68,7 @@ class DeviceTestCase(unittest.TestCase):
|
||||
self.fingerprint = fingerprint
|
||||
self.master_xpub = master_xpub
|
||||
self.password = password
|
||||
self.dev_args = ['-t', self.type, '-d', self.path]
|
||||
self.dev_args = ['-t', self.type, '-d', self.path, '--testnet']
|
||||
if emulator:
|
||||
self.emulator = emulator
|
||||
else:
|
||||
@ -113,6 +117,12 @@ class DeviceTestCase(unittest.TestCase):
|
||||
def __repr__(self):
|
||||
return '{}: {}'.format(self.full_type, super().__repr__())
|
||||
|
||||
def setup_wallets(self):
|
||||
wallet_name = '{}_{}_test'.format(self.full_type, self.id())
|
||||
self.rpc.createwallet(wallet_name=wallet_name, disable_private_keys=True, descriptors=True)
|
||||
self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}'.format(self.rpc_userpass, wallet_name))
|
||||
self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass))
|
||||
|
||||
def setUp(self):
|
||||
self.emulator.start()
|
||||
|
||||
@ -166,77 +176,64 @@ class TestDeviceConnect(DeviceTestCase):
|
||||
|
||||
class TestGetKeypool(DeviceTestCase):
|
||||
def setUp(self):
|
||||
self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(self.rpc_userpass))
|
||||
if '{}_test'.format(self.full_type) not in self.rpc.listwallets():
|
||||
self.rpc.createwallet('{}_test'.format(self.full_type), True)
|
||||
self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type))
|
||||
self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass))
|
||||
if '--testnet' not in self.dev_args:
|
||||
self.dev_args.append('--testnet')
|
||||
self.emulator.start()
|
||||
super().setUp()
|
||||
self.setup_wallets()
|
||||
|
||||
def test_getkeypool(self):
|
||||
non_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--nokeypool', '0', '20'])
|
||||
import_result = self.wpk_rpc.importmulti(non_keypool_desc)
|
||||
self.assertTrue(import_result[0]['success'])
|
||||
|
||||
pkh_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '0', '20'])
|
||||
import_result = self.wpk_rpc.importmulti(pkh_keypool_desc)
|
||||
self.assertFalse(import_result[0]['success'])
|
||||
|
||||
import_result = self.wrpc.importmulti(pkh_keypool_desc)
|
||||
import_result = self.wrpc.importdescriptors(pkh_keypool_desc)
|
||||
self.assertTrue(import_result[0]['success'])
|
||||
for i in range(0, 21):
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress())
|
||||
self.assertEqual(addr_info['hdkeypath'], "m/44'/1'/0'/0/{}".format(i))
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress())
|
||||
self.assertEqual(addr_info['hdkeypath'], "m/44'/1'/0'/1/{}".format(i))
|
||||
for _ in range(0, 21):
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'legacy'))
|
||||
self.assertTrue(addr_info['hdkeypath'].startswith("m/44'/1'/0'/0/"))
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('legacy'))
|
||||
self.assertTrue(addr_info['hdkeypath'].startswith("m/44'/1'/0'/1/"))
|
||||
|
||||
shwpkh_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '0', '20'])
|
||||
import_result = self.wrpc.importmulti(shwpkh_keypool_desc)
|
||||
import_result = self.wrpc.importdescriptors(shwpkh_keypool_desc)
|
||||
self.assertTrue(import_result[0]['success'])
|
||||
for i in range(0, 21):
|
||||
for _ in range(0, 21):
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'p2sh-segwit'))
|
||||
self.assertEqual(addr_info['hdkeypath'], "m/49'/1'/0'/0/{}".format(i))
|
||||
self.assertTrue(addr_info['hdkeypath'].startswith("m/49'/1'/0'/0/"))
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('p2sh-segwit'))
|
||||
self.assertEqual(addr_info['hdkeypath'], "m/49'/1'/0'/1/{}".format(i))
|
||||
self.assertTrue(addr_info['hdkeypath'].startswith("m/49'/1'/0'/1/"))
|
||||
|
||||
wpkh_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--wpkh', '0', '20'])
|
||||
import_result = self.wrpc.importmulti(wpkh_keypool_desc)
|
||||
import_result = self.wrpc.importdescriptors(wpkh_keypool_desc)
|
||||
self.assertTrue(import_result[0]['success'])
|
||||
for i in range(0, 21):
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress())
|
||||
self.assertEqual(addr_info['hdkeypath'], "m/84'/1'/0'/0/{}".format(i))
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress())
|
||||
self.assertEqual(addr_info['hdkeypath'], "m/84'/1'/0'/1/{}".format(i))
|
||||
for _ in range(0, 21):
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'bech32'))
|
||||
self.assertTrue(addr_info['hdkeypath'].startswith("m/84'/1'/0'/0/"))
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('bech32'))
|
||||
self.assertTrue(addr_info['hdkeypath'].startswith("m/84'/1'/0'/1/"))
|
||||
|
||||
# Test that `--all` option gives the "concatenation" of previous three calls
|
||||
all_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--all', '0', '20'])
|
||||
self.assertEqual(all_keypool_desc, pkh_keypool_desc + wpkh_keypool_desc + shwpkh_keypool_desc)
|
||||
|
||||
keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '--account', '3', '0', '20'])
|
||||
import_result = self.wrpc.importmulti(keypool_desc)
|
||||
import_result = self.wrpc.importdescriptors(keypool_desc)
|
||||
self.assertTrue(import_result[0]['success'])
|
||||
for i in range(0, 21):
|
||||
for _ in range(0, 21):
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'p2sh-segwit'))
|
||||
self.assertEqual(addr_info['hdkeypath'], "m/49'/1'/3'/0/{}".format(i))
|
||||
self.assertTrue(addr_info['hdkeypath'].startswith("m/49'/1'/3'/0/"))
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('p2sh-segwit'))
|
||||
self.assertEqual(addr_info['hdkeypath'], "m/49'/1'/3'/1/{}".format(i))
|
||||
self.assertTrue(addr_info['hdkeypath'].startswith("m/49'/1'/3'/1/"))
|
||||
keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--wpkh', '--account', '3', '0', '20'])
|
||||
import_result = self.wrpc.importmulti(keypool_desc)
|
||||
import_result = self.wrpc.importdescriptors(keypool_desc)
|
||||
self.assertTrue(import_result[0]['success'])
|
||||
for i in range(0, 21):
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress())
|
||||
self.assertEqual(addr_info['hdkeypath'], "m/84'/1'/3'/0/{}".format(i))
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress())
|
||||
self.assertEqual(addr_info['hdkeypath'], "m/84'/1'/3'/1/{}".format(i))
|
||||
for _ in range(0, 21):
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'bech32'))
|
||||
self.assertTrue(addr_info['hdkeypath'].startswith("m/84'/1'/3'/0/"))
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('bech32'))
|
||||
self.assertTrue(addr_info['hdkeypath'].startswith("m/84'/1'/3'/1/"))
|
||||
|
||||
keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--path', 'm/0h/0h/4h/*', '0', '20'])
|
||||
import_result = self.wrpc.importmulti(keypool_desc)
|
||||
import_result = self.wrpc.importdescriptors(keypool_desc)
|
||||
self.assertTrue(import_result[0]['success'])
|
||||
for i in range(0, 21):
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress())
|
||||
self.assertEqual(addr_info['hdkeypath'], "m/0'/0'/4'/{}".format(i))
|
||||
for _ in range(0, 21):
|
||||
addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'legacy'))
|
||||
self.assertTrue(addr_info['hdkeypath'].startswith("m/0'/0'/4'/"))
|
||||
|
||||
keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--path', '/0h/0h/4h/*', '0', '20'])
|
||||
self.assertEqual(keypool_desc['error'], 'Path must start with m/')
|
||||
@ -246,12 +243,6 @@ class TestGetKeypool(DeviceTestCase):
|
||||
self.assertEqual(keypool_desc['code'], -7)
|
||||
|
||||
class TestGetDescriptors(DeviceTestCase):
|
||||
def setUp(self):
|
||||
self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(self.rpc_userpass))
|
||||
if '--testnet' not in self.dev_args:
|
||||
self.dev_args.append('--testnet')
|
||||
self.emulator.start()
|
||||
|
||||
def tearDown(self):
|
||||
self.emulator.stop()
|
||||
|
||||
@ -275,14 +266,8 @@ class TestGetDescriptors(DeviceTestCase):
|
||||
|
||||
class TestSignTx(DeviceTestCase):
|
||||
def setUp(self):
|
||||
self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(self.rpc_userpass))
|
||||
if '{}_test'.format(self.full_type) not in self.rpc.listwallets():
|
||||
self.rpc.createwallet('{}_test'.format(self.full_type), True)
|
||||
self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type))
|
||||
self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass))
|
||||
if '--testnet' not in self.dev_args:
|
||||
self.dev_args.append('--testnet')
|
||||
self.emulator.start()
|
||||
super().setUp()
|
||||
self.setup_wallets()
|
||||
|
||||
def _generate_and_finalize(self, unknown_inputs, psbt):
|
||||
if not unknown_inputs:
|
||||
@ -333,49 +318,50 @@ class TestSignTx(DeviceTestCase):
|
||||
self.assertTrue(self.wrpc.testmempoolaccept([finalize_res['hex']])[0]["allowed"])
|
||||
return finalize_res['hex']
|
||||
|
||||
def _make_multisigs(self):
|
||||
desc_pubkeys = []
|
||||
sorted_pubkeys = []
|
||||
for i in range(0, 3):
|
||||
path = "/48h/1h/{}h/0/0".format(i)
|
||||
origin = '{}{}'.format(self.fingerprint, path)
|
||||
xpub = self.do_command(self.dev_args + ["--expert", "getxpub", "m{}".format(path)])
|
||||
desc_pubkeys.append("[{}]{}".format(origin, xpub["pubkey"]))
|
||||
sorted_pubkeys.append(xpub["pubkey"])
|
||||
sorted_pubkeys.sort()
|
||||
|
||||
sh_desc = AddChecksum("sh(sortedmulti(2,{},{},{}))".format(desc_pubkeys[0], desc_pubkeys[1], desc_pubkeys[2]))
|
||||
sh_ms_info = self.rpc.createmultisig(2, sorted_pubkeys, "legacy")
|
||||
self.assertEqual(self.rpc.deriveaddresses(sh_desc)[0], sh_ms_info["address"])
|
||||
|
||||
sh_wsh_desc = AddChecksum("sh(wsh(sortedmulti(2,{},{},{})))".format(desc_pubkeys[1], desc_pubkeys[2], desc_pubkeys[0]))
|
||||
sh_wsh_ms_info = self.rpc.createmultisig(2, sorted_pubkeys, "p2sh-segwit")
|
||||
self.assertEqual(self.rpc.deriveaddresses(sh_wsh_desc)[0], sh_wsh_ms_info["address"])
|
||||
|
||||
wsh_desc = AddChecksum("wsh(sortedmulti(2,{},{},{}))".format(desc_pubkeys[2], desc_pubkeys[1], desc_pubkeys[0]))
|
||||
wsh_ms_info = self.rpc.createmultisig(2, sorted_pubkeys, "bech32")
|
||||
self.assertEqual(self.rpc.deriveaddresses(wsh_desc)[0], wsh_ms_info["address"])
|
||||
|
||||
return sh_desc, sh_ms_info["address"], sh_wsh_desc, sh_wsh_ms_info["address"], wsh_desc, wsh_ms_info["address"]
|
||||
|
||||
def _test_signtx(self, input_type, multisig, external):
|
||||
# Import some keys to the watch only wallet and send coins to them
|
||||
keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '30', '50'])
|
||||
import_result = self.wrpc.importmulti(keypool_desc)
|
||||
self.assertTrue(import_result[0]['success'])
|
||||
keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '--internal', '30', '50'])
|
||||
import_result = self.wrpc.importmulti(keypool_desc)
|
||||
keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--all', '30', '50'])
|
||||
import_result = self.wrpc.importdescriptors(keypool_desc)
|
||||
self.assertTrue(import_result[0]['success'])
|
||||
sh_wpkh_addr = self.wrpc.getnewaddress('', 'p2sh-segwit')
|
||||
wpkh_addr = self.wrpc.getnewaddress('', 'bech32')
|
||||
pkh_addr = self.wrpc.getnewaddress('', 'legacy')
|
||||
self.wrpc.importaddress(wpkh_addr)
|
||||
self.wrpc.importaddress(pkh_addr)
|
||||
|
||||
# pubkeys to construct 2-of-3 multisig descriptors for import
|
||||
sh_wpkh_info = self.wrpc.getaddressinfo(sh_wpkh_addr)
|
||||
wpkh_info = self.wrpc.getaddressinfo(wpkh_addr)
|
||||
pkh_info = self.wrpc.getaddressinfo(pkh_addr)
|
||||
|
||||
# Get origin info/key pair so wallet doesn't forget how to
|
||||
# sign with keys post-import
|
||||
pubkeys = [sh_wpkh_info['desc'][8:-11],
|
||||
wpkh_info['desc'][5:-10],
|
||||
pkh_info['desc'][4:-10]]
|
||||
|
||||
# Get the descriptors with their checksums
|
||||
sh_multi_desc = self.wrpc.getdescriptorinfo('sh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + '))')['descriptor']
|
||||
sh_wsh_multi_desc = self.wrpc.getdescriptorinfo('sh(wsh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + ')))')['descriptor']
|
||||
wsh_multi_desc = self.wrpc.getdescriptorinfo('wsh(sortedmulti(2,' + pubkeys[2] + ',' + pubkeys[1] + ',' + pubkeys[0] + '))')['descriptor']
|
||||
sh_multi_desc, sh_multi_addr, sh_wsh_multi_desc, sh_wsh_multi_addr, wsh_multi_desc, wsh_multi_addr = self._make_multisigs()
|
||||
|
||||
sh_multi_import = {'desc': sh_multi_desc, "timestamp": "now", "label": "shmulti"}
|
||||
sh_wsh_multi_import = {'desc': sh_wsh_multi_desc, "timestamp": "now", "label": "shwshmulti"}
|
||||
# re-order pubkeys to allow import without "already have private keys" error
|
||||
wsh_multi_import = {'desc': wsh_multi_desc, "timestamp": "now", "label": "wshmulti"}
|
||||
multi_result = self.wrpc.importmulti([sh_multi_import, sh_wsh_multi_import, wsh_multi_import])
|
||||
multi_result = self.wrpc.importdescriptors([sh_multi_import, sh_wsh_multi_import, wsh_multi_import])
|
||||
self.assertTrue(multi_result[0]['success'])
|
||||
self.assertTrue(multi_result[1]['success'])
|
||||
self.assertTrue(multi_result[2]['success'])
|
||||
|
||||
sh_multi_addr = self.wrpc.getaddressesbylabel("shmulti").popitem()[0]
|
||||
sh_wsh_multi_addr = self.wrpc.getaddressesbylabel("shwshmulti").popitem()[0]
|
||||
wsh_multi_addr = self.wrpc.getaddressesbylabel("wshmulti").popitem()[0]
|
||||
|
||||
in_amt = 3
|
||||
out_amt = in_amt // 3
|
||||
number_inputs = 0
|
||||
@ -417,9 +403,9 @@ class TestSignTx(DeviceTestCase):
|
||||
|
||||
# Test wrapper to avoid mixed-inputs signing for Ledger
|
||||
def test_signtx(self):
|
||||
supports_mixed = {'coldcard', 'trezor_1', 'digitalbitbox', 'keepkey'}
|
||||
supports_mixed = {'coldcard', 'trezor_1', 'digitalbitbox', 'keepkey', 'trezor_t'}
|
||||
supports_multisig = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t'}
|
||||
supports_external = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard'}
|
||||
supports_external = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t'}
|
||||
self._test_signtx("legacy", self.full_type in supports_multisig, self.full_type in supports_external)
|
||||
self._test_signtx("segwit", self.full_type in supports_multisig, self.full_type in supports_external)
|
||||
if self.full_type in supports_mixed:
|
||||
@ -455,16 +441,6 @@ class TestSignTx(DeviceTestCase):
|
||||
pass
|
||||
|
||||
class TestDisplayAddress(DeviceTestCase):
|
||||
def setUp(self):
|
||||
self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(self.rpc_userpass))
|
||||
if '{}_test'.format(self.full_type) not in self.rpc.listwallets():
|
||||
self.rpc.createwallet('{}_test'.format(self.full_type), True)
|
||||
self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type))
|
||||
self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass))
|
||||
if '--testnet' not in self.dev_args:
|
||||
self.dev_args.append('--testnet')
|
||||
self.emulator.start()
|
||||
|
||||
def test_display_address_bad_args(self):
|
||||
result = self.do_command(self.dev_args + ['displayaddress', '--sh_wpkh', '--wpkh', '--path', 'm/49h/1h/0h/0/0'])
|
||||
self.assertIn('error', result)
|
||||
@ -538,152 +514,77 @@ class TestDisplayAddress(DeviceTestCase):
|
||||
self.assertIn('code', result)
|
||||
self.assertEqual(result['code'], -7)
|
||||
|
||||
def test_display_address_multisig_path(self):
|
||||
supports_multisig = {'trezor_1', 'keepkey', 'coldcard', 'trezor_t'}
|
||||
if self.full_type not in supports_multisig:
|
||||
return
|
||||
# Import some keys to the watch only wallet and get multisig address
|
||||
keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '40', '50'])
|
||||
import_result = self.wrpc.importmulti(keypool_desc)
|
||||
self.assertTrue(import_result[0]['success'])
|
||||
keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '--internal', '40', '50'])
|
||||
import_result = self.wrpc.importmulti(keypool_desc)
|
||||
self.assertTrue(import_result[0]['success'])
|
||||
sh_wpkh_addr = self.wrpc.getnewaddress('', 'p2sh-segwit')
|
||||
wpkh_addr = self.wrpc.getnewaddress('', 'bech32')
|
||||
pkh_addr = self.wrpc.getnewaddress('', 'legacy')
|
||||
self.wrpc.importaddress(wpkh_addr)
|
||||
self.wrpc.importaddress(pkh_addr)
|
||||
def _make_single_multisig(self, addrtype):
|
||||
desc_pubkeys = []
|
||||
sorted_pubkeys = []
|
||||
for i in range(0, 3):
|
||||
path = "/48h/1h/{}h/0/0".format(i)
|
||||
origin = '{}{}'.format(self.fingerprint, path)
|
||||
xpub = self.do_command(self.dev_args + ["--expert", "getxpub", "m{}".format(path)])
|
||||
desc_pubkeys.append("[{}]{}".format(origin, xpub["pubkey"]))
|
||||
sorted_pubkeys.append((xpub["pubkey"], origin))
|
||||
sorted_pubkeys.sort(key=lambda tup: tup[0])
|
||||
|
||||
# pubkeys to construct 2-of-3 multisig descriptors for import
|
||||
sh_wpkh_info = self.wrpc.getaddressinfo(sh_wpkh_addr)
|
||||
wpkh_info = self.wrpc.getaddressinfo(wpkh_addr)
|
||||
pkh_info = self.wrpc.getaddressinfo(pkh_addr)
|
||||
if addrtype == "pkh":
|
||||
desc = AddChecksum("sh(sortedmulti(2,{},{},{}))".format(desc_pubkeys[0], desc_pubkeys[1], desc_pubkeys[2]))
|
||||
ms_info = self.rpc.createmultisig(2, [x[0] for x in sorted_pubkeys], "legacy")
|
||||
elif addrtype == "sh_wpkh":
|
||||
desc = AddChecksum("sh(wsh(sortedmulti(2,{},{},{})))".format(desc_pubkeys[1], desc_pubkeys[2], desc_pubkeys[0]))
|
||||
ms_info = self.rpc.createmultisig(2, [x[0] for x in sorted_pubkeys], "p2sh-segwit")
|
||||
elif addrtype == "wpkh":
|
||||
desc = AddChecksum("wsh(sortedmulti(2,{},{},{}))".format(desc_pubkeys[2], desc_pubkeys[1], desc_pubkeys[0]))
|
||||
ms_info = self.rpc.createmultisig(2, [x[0] for x in sorted_pubkeys], "bech32")
|
||||
else:
|
||||
self.fail("Oops the test is broken")
|
||||
|
||||
pubkeys = [sh_wpkh_info['desc'][8:-11],
|
||||
wpkh_info['desc'][5:-10],
|
||||
pkh_info['desc'][4:-10]]
|
||||
self.assertEqual(self.rpc.deriveaddresses(desc)[0], ms_info["address"])
|
||||
|
||||
# Get the descriptors with their checksums
|
||||
sh_multi_desc = self.wrpc.getdescriptorinfo('sh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + '))')['descriptor']
|
||||
sh_wsh_multi_desc = self.wrpc.getdescriptorinfo('sh(wsh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + ')))')['descriptor']
|
||||
wsh_multi_desc = self.wrpc.getdescriptorinfo('wsh(sortedmulti(2,' + pubkeys[2] + ',' + pubkeys[1] + ',' + pubkeys[0] + '))')['descriptor']
|
||||
path = "{},{},{}".format(sorted_pubkeys[0][1], sorted_pubkeys[1][1], sorted_pubkeys[2][1])
|
||||
|
||||
sh_multi_import = {'desc': sh_multi_desc, "timestamp": "now", "label": "shmulti-display"}
|
||||
sh_wsh_multi_import = {'desc': sh_wsh_multi_desc, "timestamp": "now", "label": "shwshmulti-display"}
|
||||
# re-order pubkeys to allow import without "already have private keys" error
|
||||
wsh_multi_import = {'desc': wsh_multi_desc, "timestamp": "now", "label": "wshmulti-display"}
|
||||
multi_result = self.wrpc.importmulti([sh_multi_import, sh_wsh_multi_import, wsh_multi_import])
|
||||
self.assertTrue(multi_result[0]['success'])
|
||||
self.assertTrue(multi_result[1]['success'])
|
||||
self.assertTrue(multi_result[2]['success'])
|
||||
return ms_info["address"], desc, ms_info["redeemScript"], path
|
||||
|
||||
sh_multi_addr = self.wrpc.getaddressesbylabel("shmulti-display").popitem()[0]
|
||||
sh_wsh_multi_addr = self.wrpc.getaddressesbylabel("shwshmulti-display").popitem()[0]
|
||||
wsh_multi_addr = self.wrpc.getaddressesbylabel("wshmulti-display").popitem()[0]
|
||||
def test_display_address_multisig(self):
|
||||
if self.full_type not in SUPPORTS_MS_DISPLAY:
|
||||
raise unittest.SkipTest("{} does not support multisig display".format(self.full_type))
|
||||
|
||||
sh_multi_addr_redeem_script = self.wrpc.getaddressinfo(sh_multi_addr)['hex']
|
||||
sh_wsh_multi_addr_redeem_script = self.wrpc.getaddressinfo(sh_multi_addr)['hex']
|
||||
wsh_multi_addr_redeem_script = self.wrpc.getaddressinfo(sh_multi_addr)['hex']
|
||||
for addrtype in ["pkh", "sh_wpkh", "wpkh"]:
|
||||
for use_desc in [True, False]:
|
||||
with self.subTest(addrtype=addrtype, use_desc=use_desc):
|
||||
addr, desc, rs, path = self._make_single_multisig(addrtype)
|
||||
|
||||
path = pubkeys[2][1:24] + ',' + pubkeys[1][1:24] + ',' + pubkeys[0][1:24]
|
||||
# need to replace `'` with `h` for stdin option to work
|
||||
path = path.replace("'", "h")
|
||||
if use_desc:
|
||||
args = ['displayaddress', '--desc', desc]
|
||||
else:
|
||||
args = ['displayaddress', '--path', path, '--redeem_script', rs]
|
||||
if addrtype != "pkh":
|
||||
args.append("--{}".format(addrtype))
|
||||
|
||||
# legacy
|
||||
result = self.do_command(self.dev_args + ['displayaddress', '--path', path, '--redeem_script', sh_multi_addr_redeem_script])
|
||||
self.assertNotIn('error', result)
|
||||
self.assertNotIn('code', result)
|
||||
self.assertIn('address', result)
|
||||
self.assertEqual(sh_multi_addr, result['address'])
|
||||
|
||||
# wrapped segwit
|
||||
result = self.do_command(self.dev_args + ['displayaddress', '--sh_wpkh', '--path', path, '--redeem_script', sh_wsh_multi_addr_redeem_script])
|
||||
self.assertNotIn('error', result)
|
||||
self.assertNotIn('code', result)
|
||||
self.assertIn('address', result)
|
||||
self.assertEqual(sh_wsh_multi_addr, result['address'])
|
||||
|
||||
# native setwit
|
||||
result = self.do_command(self.dev_args + ['displayaddress', '--wpkh', '--path', path, '--redeem_script', wsh_multi_addr_redeem_script])
|
||||
result = self.do_command(self.dev_args + args)
|
||||
self.assertNotIn('error', result)
|
||||
self.assertNotIn('code', result)
|
||||
self.assertIn('address', result)
|
||||
|
||||
if addrtype == "wpkh":
|
||||
# removes prefix and checksum since regtest gives
|
||||
# prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix
|
||||
self.assertEqual(addr[4:58], result['address'][2:56])
|
||||
else:
|
||||
self.assertEqual(addr, result['address'])
|
||||
|
||||
def test_display_address_xpub_multisig(self):
|
||||
if self.full_type not in SUPPORTS_XPUB_MS_DISPLAY:
|
||||
raise unittest.SkipTest("{} does not support multsig display with xpubs".format(self.full_type))
|
||||
|
||||
account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/48h/1h/0h'])['xpub']
|
||||
desc = 'wsh(multi(2,[' + self.fingerprint + '/48h/1h/0h]' + account_xpub + '/0/0,[' + self.fingerprint + '/48h/1h/0h]' + account_xpub + '/1/0))'
|
||||
result = self.do_command(self.dev_args + ['displayaddress', '--desc', desc])
|
||||
self.assertNotIn('error', result)
|
||||
self.assertNotIn('code', result)
|
||||
self.assertIn('address', result)
|
||||
addr = self.rpc.deriveaddresses(AddChecksum(desc))[0]
|
||||
# removes prefix and checksum since regtest gives
|
||||
# prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix
|
||||
self.assertEqual(wsh_multi_addr[4:58], result['address'][2:56])
|
||||
|
||||
def test_display_address_multisig_descriptor(self):
|
||||
supports_multisig = {'trezor_1', 'keepkey', 'coldcard', 'trezor_t'}
|
||||
if self.full_type not in supports_multisig:
|
||||
return
|
||||
# Import some keys to the watch only wallet and get multisig address
|
||||
keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '50', '60'])
|
||||
import_result = self.wrpc.importmulti(keypool_desc)
|
||||
self.assertTrue(import_result[0]['success'])
|
||||
keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '--internal', '50', '60'])
|
||||
import_result = self.wrpc.importmulti(keypool_desc)
|
||||
self.assertTrue(import_result[0]['success'])
|
||||
sh_wpkh_addr = self.wrpc.getnewaddress('', 'p2sh-segwit')
|
||||
wpkh_addr = self.wrpc.getnewaddress('', 'bech32')
|
||||
pkh_addr = self.wrpc.getnewaddress('', 'legacy')
|
||||
self.wrpc.importaddress(wpkh_addr)
|
||||
self.wrpc.importaddress(pkh_addr)
|
||||
|
||||
# pubkeys to construct 2-of-3 multisig descriptors for import
|
||||
sh_wpkh_info = self.wrpc.getaddressinfo(sh_wpkh_addr)
|
||||
wpkh_info = self.wrpc.getaddressinfo(wpkh_addr)
|
||||
pkh_info = self.wrpc.getaddressinfo(pkh_addr)
|
||||
|
||||
pubkeys = [sh_wpkh_info['desc'][8:-11],
|
||||
wpkh_info['desc'][5:-10],
|
||||
pkh_info['desc'][4:-10]]
|
||||
|
||||
# Get the descriptors with their checksums
|
||||
sh_multi_desc = self.wrpc.getdescriptorinfo('sh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + '))')['descriptor']
|
||||
sh_wsh_multi_desc = self.wrpc.getdescriptorinfo('sh(wsh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + ')))')['descriptor']
|
||||
wsh_multi_desc = self.wrpc.getdescriptorinfo('wsh(sortedmulti(2,' + pubkeys[2] + ',' + pubkeys[1] + ',' + pubkeys[0] + '))')['descriptor']
|
||||
|
||||
sh_multi_import = {'desc': sh_multi_desc, "timestamp": "now", "label": "shmulti-display-desc"}
|
||||
sh_wsh_multi_import = {'desc': sh_wsh_multi_desc, "timestamp": "now", "label": "shwshmulti-display-desc"}
|
||||
# re-order pubkeys to allow import without "already have private keys" error
|
||||
wsh_multi_import = {'desc': wsh_multi_desc, "timestamp": "now", "label": "wshmulti-display-desc"}
|
||||
multi_result = self.wrpc.importmulti([sh_multi_import, sh_wsh_multi_import, wsh_multi_import])
|
||||
self.assertTrue(multi_result[0]['success'])
|
||||
self.assertTrue(multi_result[1]['success'])
|
||||
self.assertTrue(multi_result[2]['success'])
|
||||
|
||||
sh_multi_addr = self.wrpc.getaddressesbylabel("shmulti-display-desc").popitem()[0]
|
||||
sh_wsh_multi_addr = self.wrpc.getaddressesbylabel("shwshmulti-display-desc").popitem()[0]
|
||||
wsh_multi_addr = self.wrpc.getaddressesbylabel("wshmulti-display-desc").popitem()[0]
|
||||
|
||||
# need to replace `'` with `h` and to remove checksome for the stdin option to work
|
||||
sh_multi_desc = sh_multi_desc.replace("'", "h").split('#')[0]
|
||||
sh_wsh_multi_desc = sh_wsh_multi_desc.replace("'", "h").split('#')[0]
|
||||
wsh_multi_desc = wsh_multi_desc.replace("'", "h").split('#')[0]
|
||||
|
||||
# legacy
|
||||
result = self.do_command(self.dev_args + ['displayaddress', '--desc', sh_multi_desc])
|
||||
self.assertNotIn('error', result)
|
||||
self.assertNotIn('code', result)
|
||||
self.assertIn('address', result)
|
||||
self.assertEqual(sh_multi_addr, result['address'])
|
||||
|
||||
# wrapped segwit
|
||||
result = self.do_command(self.dev_args + ['displayaddress', '--desc', sh_wsh_multi_desc])
|
||||
self.assertNotIn('error', result)
|
||||
self.assertNotIn('code', result)
|
||||
self.assertIn('address', result)
|
||||
self.assertEqual(sh_wsh_multi_addr, result['address'])
|
||||
|
||||
# native setwit
|
||||
result = self.do_command(self.dev_args + ['displayaddress', '--desc', wsh_multi_desc])
|
||||
self.assertNotIn('error', result)
|
||||
self.assertNotIn('code', result)
|
||||
self.assertIn('address', result)
|
||||
# removes prefix and checksum since regtest gives
|
||||
# prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix
|
||||
self.assertEqual(wsh_multi_addr[4:58], result['address'][2:56])
|
||||
self.assertEqual(addr[4:58], result['address'][2:56])
|
||||
|
||||
class TestSignMessage(DeviceTestCase):
|
||||
def test_sign_msg(self):
|
||||
|
||||
@ -28,7 +28,7 @@ def digitalbitbox_test_suite(simulator, rpc, userpass, interface):
|
||||
reply = send_plain(b'{"password":"0000"}', dev)
|
||||
if 'error' not in reply:
|
||||
break
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
# Cleanup
|
||||
@ -133,6 +133,9 @@ def digitalbitbox_test_suite(simulator, rpc, userpass, interface):
|
||||
self.assertTrue(result['success'])
|
||||
|
||||
class TestBitboxGetXpub(DeviceTestCase):
|
||||
def setUp(self):
|
||||
self.dev_args.remove('--testnet')
|
||||
|
||||
def test_getxpub(self):
|
||||
result = self.do_command(self.dev_args + ['--expert', 'getxpub', 'm/44h/0h/0h/3'])
|
||||
self.assertEqual(result['xpub'], 'xpub6Du9e5Cz1NZWz3dvsvM21tsj4xEdbAb7AcbysFL42Y3yr8PLMnsaxhetHxurTpX5Rp5RbnFFwP1wct8K3gErCUSwcxFhxThsMBSxdmkhTNf')
|
||||
|
||||
@ -99,6 +99,9 @@ def ledger_test_suite(emulator, rpc, userpass, interface):
|
||||
self.assertEqual(result['code'], -9)
|
||||
|
||||
class TestLedgerGetXpub(DeviceTestCase):
|
||||
def setUp(self):
|
||||
self.dev_args.remove("--testnet")
|
||||
|
||||
def test_getxpub(self):
|
||||
result = self.do_command(self.dev_args + ['--expert', 'getxpub', 'm/44h/0h/0h/3'])
|
||||
self.assertEqual(result['xpub'], 'xpub6DqTtMuqBiBsSPb5UxB1qgJ3ViXuhoyZYhw3zTK4MywLB6psioW4PN1SAbhxVVirKQojnTBsjG5gXiiueRBgWmUuN43dpbMSgMCQHVqx2bR')
|
||||
|
||||
@ -16,7 +16,7 @@ class TestUdevRulesInstaller(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(self):
|
||||
for root, dirs, files in walk(self.INSTALLATION_FOLDER, topdown=False):
|
||||
for root, _, files in walk(self.INSTALLATION_FOLDER, topdown=False):
|
||||
for name in files:
|
||||
remove(path.join(root, name))
|
||||
removedirs(self.INSTALLATION_FOLDER)
|
||||
@ -28,7 +28,7 @@ class TestUdevRulesInstaller(unittest.TestCase):
|
||||
self.assertEqual(result['error'], 'Need to be root.')
|
||||
self.assertEqual(result['code'], -16)
|
||||
# Assert files wre copied
|
||||
for root, dirs, files in walk(self.INSTALLATION_FOLDER, topdown=False):
|
||||
for _, _, files in walk(self.INSTALLATION_FOLDER, topdown=False):
|
||||
for file_name in files:
|
||||
src = path.join(self.SOURCE_FOLDER, file_name)
|
||||
tgt = path.join(self.INSTALLATION_FOLDER, file_name)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user