Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor logic + cleanup + bug fixes #160

Merged
merged 31 commits into from
Jun 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f86e492
Refactor logic.py
ben-kaufman Jun 7, 2020
ba9a2fc
Add tests for device, device_manager, and wallet_manager
ben-kaufman Jun 9, 2020
792fb1e
Fix loading wallets
ben-kaufman Jun 9, 2020
7981e2b
Fix save settings bug
ben-kaufman Jun 9, 2020
131616a
Fix labels support for Bitcoin Core v0.20.0
ben-kaufman Jun 9, 2020
1856701
Add Ledger and Trezor device logos
ben-kaufman Jun 10, 2020
0fb0722
Fix broadcast tx
ben-kaufman Jun 12, 2020
927c1ee
Device refactoring
ben-kaufman Jun 12, 2020
646de25
Refactor wallet object
ben-kaufman Jun 13, 2020
2211da6
Remove create wallet duplicated code
ben-kaufman Jun 14, 2020
2072da9
Move qr and sd psbt generation to Device
ben-kaufman Jun 14, 2020
3f1abe6
Prevent removing device used in a wallet
ben-kaufman Jun 14, 2020
f55d3df
Migrate older device JSON format
ben-kaufman Jun 14, 2020
f202590
Fix new wallet icons bug
ben-kaufman Jun 14, 2020
ab92b8c
Move back from folders to root
ben-kaufman Jun 14, 2020
e7a0970
Seperate format migration code
ben-kaufman Jun 15, 2020
10852dd
Separate devices into classes
ben-kaufman Jun 16, 2020
507b107
Fix specter bug
ben-kaufman Jun 16, 2020
3eec641
Select device type
ben-kaufman Jun 17, 2020
2d4b6f1
Support wallet export format
ben-kaufman Jun 17, 2020
370f125
Remove sortedmulti
ben-kaufman Jun 17, 2020
8155940
Merge branch 'master' into refactor-logic
ben-kaufman Jun 17, 2020
2e96d5a
Fix the tests
ben-kaufman Jun 18, 2020
72d38aa
Remove mistake @property
ben-kaufman Jun 18, 2020
e7a220f
update multi and sortedmulti import
stepansnigirev Jun 18, 2020
e136075
Merge branch 'refactor-logic' of https://github.com/ben-kaufman/spect…
stepansnigirev Jun 18, 2020
3de255a
fix descriptor replace bug
stepansnigirev Jun 18, 2020
f5b3941
add reject reason to the error on broadcast
stepansnigirev Jun 18, 2020
6779fb5
Merge branch 'master' into refactor-logic
stepansnigirev Jun 18, 2020
3a10657
Restore sortedmulti old format migrate
ben-kaufman Jun 18, 2020
b94d35b
Merge branch 'master' into refactor-logic
ben-kaufman Jun 18, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
391 changes: 173 additions & 218 deletions src/cryptoadvance/specter/controller.py

Large diffs are not rendered by default.

127 changes: 127 additions & 0 deletions src/cryptoadvance/specter/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import json
from .helpers import decode_base58, hash160
from .serializations import PSBT
from .key import Key


class Device:
QR_CODE_TYPES = ['specter', 'other']
SD_CARD_TYPES = ['coldcard', 'other']
HWI_TYPES = ['keepkey', 'ledger', 'trezor', 'specter', 'coldcard']

def __init__(self, name, alias, device_type, keys, fullpath, manager):
self.name = name
self.alias = alias
self.device_type = device_type
self.keys = keys
self.fullpath = fullpath
self.manager = manager

@classmethod
def from_json(cls, device_dict, manager, default_alias='', default_fullpath=''):
name = device_dict['name'] if 'name' in device_dict else ''
alias = device_dict['alias'] if 'alias' in device_dict else default_alias
device_type = device_dict['type'] if 'type' in device_dict else ''
keys = [Key.from_json(key_dict) for key_dict in device_dict['keys']]
fullpath = device_dict['fullpath'] if 'fullpath' in device_dict else default_fullpath
return cls(name, alias, device_type, keys, fullpath, manager)

@property
def json(self):
return {
"name": self.name,
"alias": self.alias,
"type": self.device_type,
"keys": [key.json for key in self.keys],
"fullpath": self.fullpath,
}

def _update_keys(self):
with open(self.fullpath, "r") as f:
content = json.loads(f.read())
content['keys'] = [key.json for key in self.keys]
with open(self.fullpath, "w") as f:
f.write(json.dumps(content,indent=4))
self.manager.update()

def remove_key(self, key):
self.keys = [k for k in self.keys if k != key]
self._update_keys()

def add_keys(self, keys):
for key in keys:
if key not in self.keys:
self.keys.append(key)
self._update_keys()

def wallets(self, wallet_manager):
wallets = []
for wallet in wallet_manager.wallets.values():
if self in wallet.devices:
wallets.append(wallet)
return wallets

@staticmethod
def create_sdcard_psbt(base64_psbt, keys):
sdcard_psbt = PSBT()
sdcard_psbt.deserialize(base64_psbt)
if len(keys) > 1:
for k in keys:
key = b'\x01' + decode_base58(k.xpub)
if k.fingerprint != '':
fingerprint = bytes.fromhex(k.fingerprint)
else:
fingerprint = _get_xpub_fingerprint(k.xpub)
if k.derivation != '':
der = _der_to_bytes(k.derivation)
else:
der = b''
value = fingerprint + der
sdcard_psbt.unknown[key] = value
return sdcard_psbt.serialize()

@staticmethod
def create_qrcode_psbt(base64_psbt, fingerprint):
qr_psbt = PSBT()
qr_psbt.deserialize(base64_psbt)
for inp in qr_psbt.inputs + qr_psbt.outputs:
inp.witness_script = b""
inp.redeem_script = b""
if len(inp.hd_keypaths) > 0:
k = list(inp.hd_keypaths.keys())[0]
# proprietary field - wallet derivation path
# only contains two last derivation indexes - change and index
inp.unknown[b"\xfc\xca\x01" + fingerprint] = b"".join([i.to_bytes(4, "little") for i in inp.hd_keypaths[k][-2:]])
inp.hd_keypaths = {}
return qr_psbt.serialize()

def __eq__(self, other):
if other is None:
return False
return self.alias == other.alias

def __hash__(self):
return hash(self.alias)


def _get_xpub_fingerprint(xpub):
b = decode_base58(xpub)
return hash160(b[-33:])[:4]

def _der_to_bytes(derivation):
items = derivation.split("/")
if len(items) == 0:
return b''
if items[0] == 'm':
items = items[1:]
if items[-1] == '':
items = items[:-1]
res = b''
for item in items:
index = 0
if item[-1] == 'h' or item[-1] == "'":
index += 0x80000000
item = item[:-1]
index += int(item)
res += index.to_bytes(4,'big')
return res
86 changes: 86 additions & 0 deletions src/cryptoadvance/specter/device_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import os, json, logging
from .device import Device
from .devices.coldcard import ColdCard
from .devices.trezor import Trezor
from .devices.ledger import Ledger
from .devices.keepkey import Keepkey
from .devices.specter import Specter
from .helpers import alias, load_jsons


logger = logging.getLogger(__name__)

device_classes = {
'coldcard': ColdCard,
'trezor': Trezor,
'keepkey': Keepkey,
'ledger': Ledger,
'specter': Specter
}

def get_device_class(device_type):
if device_type in device_classes:
return device_classes[device_type]
return Device

class DeviceManager:
''' A DeviceManager mainly manages the persistence of a device-json-structures
compliant to helper.load_jsons
'''
# of them via json-files in an empty data folder
def __init__(self, data_folder):
if data_folder is not None:
self.data_folder = data_folder
if data_folder.startswith("~"):
data_folder = os.path.expanduser(data_folder)
# creating folders if they don't exist
if not os.path.isdir(data_folder):
os.mkdir(data_folder)
self.update()

def update(self):
self.devices = {}
devices_files = load_jsons(self.data_folder, key="name")
for device_alias in devices_files:
fullpath = os.path.join(self.data_folder, "%s.json" % device_alias)
self.devices[devices_files[device_alias]["name"]] = get_device_class(devices_files[device_alias]["type"]).from_json(
devices_files[device_alias],
self,
default_alias=device_alias,
default_fullpath=fullpath
)

@property
def devices_names(self):
return sorted(self.devices.keys())

def add_device(self, name, device_type, keys):
device_alias = alias(name)
fullpath = os.path.join(self.data_folder, "%s.json" % device_alias)
i = 2
while os.path.isfile(fullpath):
device_alias = alias("%s %d" % (name, i))
fullpath = os.path.join(self.data_folder, "%s.json" % device_alias)
i += 1
# remove duplicated keys if any exist
non_dup_keys = []
for key in keys:
if key not in non_dup_keys:
non_dup_keys.append(key)
keys = non_dup_keys
device = get_device_class(device_type)(name, device_alias, device_type, keys, fullpath, self)
with open(fullpath, "w") as file:
file.write(json.dumps(device.json, indent=4))

self.update() # reload files
return device

def get_by_alias(self, device_alias):
for device_name in self.devices:
if self.devices[device_name].alias == device_alias:
return self.devices[device_name]
logger.error("Could not find Device %s" % device_alias)

def remove_device(self, device):
os.remove(device.fullpath)
self.update()
53 changes: 53 additions & 0 deletions src/cryptoadvance/specter/devices/coldcard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import urllib
from .sd_card_device import SDCardDevice
from ..helpers import get_xpub_fingerprint


CC_TYPES = {
'legacy': 'BIP45',
'p2sh-segwit': 'P2WSH-P2SH',
'bech32': 'P2WSH'
}
class ColdCard(SDCardDevice):
def __init__(self, name, alias, device_type, keys, fullpath, manager):
SDCardDevice.__init__(self, name, alias, 'coldcard', keys, fullpath, manager)
self.sd_card_support = True
self.qr_code_support = False
self.wallet_export_type = 'file'

def create_psbts(self, base64_psbt, wallet):
psbts = SDCardDevice.create_psbts(self, base64_psbt, wallet)
return psbts

def export_wallet(self, wallet):
CC_TYPES = {
'legacy': 'BIP45',
'p2sh-segwit': 'P2WSH-P2SH',
'bech32': 'P2WSH'
}
# try to find at least one derivation
# cc assume the same derivation for all keys :(
derivation = None
for k in wallet.keys:
if k.derivation != '':
derivation = k.derivation.replace("h","'")
break
if derivation is None:
return None
cc_file = """# Coldcard Multisig setup file (created on Specter Desktop)
#
Name: {}
Policy: {} of {}
Derivation: {}
Format: {}
""".format(wallet.name, wallet.sigs_required,
len(wallet.keys), derivation,
CC_TYPES[wallet.address_type]
)
for k in wallet.keys:
# cc assumes fingerprint is known
fingerprint = k.fingerprint
if fingerprint == '':
fingerprint = get_xpub_fingerprint(k.xpub).hex()
cc_file += "{}: {}\n".format(fingerprint.upper(), k.xpub)
return urllib.parse.quote(cc_file)
11 changes: 11 additions & 0 deletions src/cryptoadvance/specter/devices/hwi_device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from ..device import Device


class HWIDevice(Device):
def __init__(self, name, alias, device_type, keys, fullpath, manager):
Device.__init__(self, name, alias, device_type, keys, fullpath, manager)
self.hwi_support = True
self.exportable_to_wallet = False

def create_psbts(self, base64_psbt, wallet):
return { 'hwi': base64_psbt }
7 changes: 7 additions & 0 deletions src/cryptoadvance/specter/devices/keepkey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .hwi_device import HWIDevice

class Keepkey(HWIDevice):
def __init__(self, name, alias, device_type, keys, fullpath, manager):
HWIDevice.__init__(self, name, alias, 'keepkey', keys, fullpath, manager)
self.sd_card_support = False
self.qr_code_support = False
7 changes: 7 additions & 0 deletions src/cryptoadvance/specter/devices/ledger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .hwi_device import HWIDevice

class Ledger(HWIDevice):
def __init__(self, name, alias, device_type, keys, fullpath, manager):
HWIDevice.__init__(self, name, alias, 'ledger', keys, fullpath, manager)
self.sd_card_support = False
self.qr_code_support = False
52 changes: 52 additions & 0 deletions src/cryptoadvance/specter/devices/sd_card_device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from .hwi_device import HWIDevice
from ..helpers import decode_base58, get_xpub_fingerprint, hash160
from ..serializations import PSBT


class SDCardDevice(HWIDevice):
def __init__(self, name, alias, device_type, keys, fullpath, manager):
HWIDevice.__init__(self, name, alias, device_type, keys, fullpath, manager)
self.sd_card_support = True
self.exportable_to_wallet = True

def create_psbts(self, base64_psbt, wallet):
psbts = HWIDevice.create_psbts(self, base64_psbt, wallet)
sdcard_psbt = PSBT()
sdcard_psbt.deserialize(base64_psbt)
if len(wallet.keys) > 1:
for k in wallet.keys:
key = b'\x01' + decode_base58(k.xpub)
if k.fingerprint != '':
fingerprint = bytes.fromhex(k.fingerprint)
else:
fingerprint = _get_xpub_fingerprint(k.xpub)
if k.derivation != '':
der = _der_to_bytes(k.derivation)
else:
der = b''
value = fingerprint + der
sdcard_psbt.unknown[key] = value
psbts['sdcard'] = sdcard_psbt.serialize()
return psbts

def _get_xpub_fingerprint(xpub):
b = decode_base58(xpub)
return hash160(b[-33:])[:4]

def _der_to_bytes(derivation):
items = derivation.split("/")
if len(items) == 0:
return b''
if items[0] == 'm':
items = items[1:]
if items[-1] == '':
items = items[:-1]
res = b''
for item in items:
index = 0
if item[-1] == 'h' or item[-1] == "'":
index += 0x80000000
item = item[:-1]
index += int(item)
res += index.to_bytes(4,'big')
return res
39 changes: 39 additions & 0 deletions src/cryptoadvance/specter/devices/specter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import hashlib
from .sd_card_device import SDCardDevice
from ..serializations import PSBT


class Specter(SDCardDevice):
def __init__(self, name, alias, device_type, keys, fullpath, manager):
SDCardDevice.__init__(self, name, alias, 'specter', keys, fullpath, manager)
self.sd_card_support = False
self.qr_code_support = True
self.wallet_export_type = 'qr'

def create_psbts(self, base64_psbt, wallet):
psbts = SDCardDevice.create_psbts(self, base64_psbt, wallet)
qr_psbt = PSBT()
qr_psbt.deserialize(base64_psbt)
for inp in qr_psbt.inputs + qr_psbt.outputs:
inp.witness_script = b""
inp.redeem_script = b""
if len(inp.hd_keypaths) > 0:
k = list(inp.hd_keypaths.keys())[0]
# proprietary field - wallet derivation path
# only contains two last derivation indexes - change and index
inp.unknown[b"\xfc\xca\x01" + get_wallet_fingerprint(wallet)] = b"".join([i.to_bytes(4, "little") for i in inp.hd_keypaths[k][-2:]])
inp.hd_keypaths = {}
psbts['qrcode'] = qr_psbt.serialize()
return psbts

def export_wallet(self, wallet):
return wallet.name + "&" + get_wallet_qr_descriptor(wallet)

def get_wallet_qr_descriptor(wallet):
return wallet.recv_descriptor.split("#")[0].replace("/0/*", "")

def get_wallet_fingerprint(wallet):
""" Unique fingerprint of the wallet - first 4 bytes of hash160 of its descriptor """
h256 = hashlib.sha256(get_wallet_qr_descriptor(wallet).encode()).digest()
h160 = hashlib.new('ripemd160', h256).digest()
return h160[:4]
7 changes: 7 additions & 0 deletions src/cryptoadvance/specter/devices/trezor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .hwi_device import HWIDevice

class Trezor(HWIDevice):
def __init__(self, name, alias, device_type, keys, fullpath, manager):
HWIDevice.__init__(self, name, alias, 'trezor', keys, fullpath, manager)
self.sd_card_support = False
self.qr_code_support = False
Loading