Skip to content

Commit

Permalink
[ADD] Method to get purpose, encoding, key_path from witness_type
Browse files Browse the repository at this point in the history
  • Loading branch information
mccwdev committed Oct 13, 2023
1 parent 86bb946 commit 33c2ed7
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 46 deletions.
18 changes: 4 additions & 14 deletions bitcoinlib/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ def addr_convert(addr, prefix, encoding=None, to_encoding=None):
return pubkeyhash_to_addr(pkh, prefix=prefix, encoding=to_encoding)


def path_expand(path, path_template=None, level_offset=None, account_id=0, cosigner_id=0, purpose=44,
def path_expand(path, path_template=None, level_offset=None, account_id=0, cosigner_id=0, purpose=84,
address_index=0, change=0, witness_type=DEFAULT_WITNESS_TYPE, multisig=False, network=DEFAULT_NETWORK):
"""
Create key path. Specify part of key path and path settings
Expand Down Expand Up @@ -391,11 +391,7 @@ def path_expand(path, path_template=None, level_offset=None, account_id=0, cosig
if isinstance(path, TYPE_TEXT):
path = path.split('/')
if not path_template:
ks = [k for k in WALLET_KEY_STRUCTURES if
k['witness_type'] == witness_type and k['multisig'] == multisig and k['purpose'] is not None]
if ks:
purpose = ks[0]['purpose']
path_template = ks[0]['key_path']
path_template, purpose, _ = get_key_structure_data(witness_type, multisig)
if not isinstance(path, list):
raise BKeyError("Please provide path as list with at least 1 item. Wallet key path format is %s" %
path_template)
Expand Down Expand Up @@ -1785,14 +1781,8 @@ def public_master(self, account_id=0, purpose=None, multisig=None, witness_type=
self.multisig = multisig
if witness_type:
self.witness_type = witness_type
ks = [k for k in WALLET_KEY_STRUCTURES if
k['witness_type'] == self.witness_type and k['multisig'] == self.multisig and k['purpose'] is not None]
if len(ks) > 1:
raise BKeyError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for "
"witness_type - multisig combination")
if ks and not purpose:
purpose = ks[0]['purpose']
path_template = ks[0]['key_path']

path_template, purpose, _ = get_key_structure_data(self.witness_type, self.multisig, purpose)

# Use last hardened key as public master root
pm_depth = path_template.index([x for x in path_template if x[-1:] == "'"][-1]) + 1
Expand Down
17 changes: 17 additions & 0 deletions bitcoinlib/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,23 @@ def get_encoding_from_witness(witness_type=None):
raise ValueError("Unknown witness type %s" % witness_type)


def get_key_structure_data(witness_type, multisig=False, purpose=None, encoding=None):
if not witness_type:
return None, None, None
ks = [k for k in WALLET_KEY_STRUCTURES if
k['witness_type'] == witness_type and k['multisig'] == multisig and k['purpose'] is not None]
if len(ks) > 1:
raise ValueError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for "
"witness_type - multisig combination: %s, %s" % (witness_type, multisig))
if not ks:
raise ValueError("Please check definitions in WALLET_KEY_STRUCTURES. No options found for "
"witness_type - multisig combination: %s, %s" % (witness_type, multisig))
purpose = ks[0]['purpose'] if not purpose else purpose
path_template = ks[0]['key_path']
encoding = ks[0]['encoding'] if not encoding else encoding
return path_template, purpose, encoding


def deprecated(func):
"""
This is a decorator which can be used to mark functions as deprecated. It will result in a warning being emitted when the function is used.
Expand Down
78 changes: 46 additions & 32 deletions bitcoinlib/wallets.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ class WalletKey(object):
"""

@staticmethod
def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0, purpose=44, parent_id=0,
def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0, purpose=84, parent_id=0,
path='m', key_type=None, encoding=None, witness_type=DEFAULT_WITNESS_TYPE, multisig=False,
cosigner_id=None):
"""
Expand Down Expand Up @@ -331,7 +331,7 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0
:type network: str
:param change: Use 0 for normal key, and 1 for change key (for returned payments)
:type change: int
:param purpose: BIP0044 purpose field, default is 44
:param purpose: BIP0044 purpose field, default is 84
:type purpose: int
:param parent_id: Key ID of parent, default is 0 (no parent)
:type parent_id: int
Expand Down Expand Up @@ -405,7 +405,8 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0
account_id=account_id, depth=k.depth, change=change, address_index=k.child_index,
wif=k.wif(witness_type=witness_type, multisig=multisig, is_private=True), address=address,
parent_id=parent_id, compressed=k.compressed, is_private=k.is_private, path=path,
key_type=key_type, network_name=network, encoding=encoding, cosigner_id=cosigner_id)
key_type=key_type, network_name=network, encoding=encoding, cosigner_id=cosigner_id,
witness_type=witness_type)
else:
keyexists = session.query(DbKey).\
filter(DbKey.wallet_id == wallet_id,
Expand All @@ -416,7 +417,8 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0
nk = DbKey(name=name[:80], wallet_id=wallet_id, purpose=purpose,
account_id=account_id, depth=k.depth, change=change, address=k.address,
parent_id=parent_id, compressed=k.compressed, is_private=False, path=path,
key_type=key_type, network_name=network, encoding=encoding, cosigner_id=cosigner_id)
key_type=key_type, network_name=network, encoding=encoding, cosigner_id=cosigner_id,
witness_type=witness_type)

session.merge(DbNetwork(name=network))
session.add(nk)
Expand Down Expand Up @@ -1261,16 +1263,17 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0
key_path = ['m']
purpose = 0
else:
ks = [k for k in WALLET_KEY_STRUCTURES if k['witness_type'] == witness_type and
k['multisig'] == multisig and k['purpose'] is not None]
if len(ks) > 1:
raise WalletError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for "
"witness_type - multisig combination")
if ks and not purpose:
purpose = ks[0]['purpose']
if ks and not encoding:
encoding = ks[0]['encoding']
key_path = ks[0]['key_path']
# ks = [k for k in WALLET_KEY_STRUCTURES if k['witness_type'] == witness_type and
# k['multisig'] == multisig and k['purpose'] is not None]
# if len(ks) > 1:
# raise WalletError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for "
# "witness_type - multisig combination")
# if ks and not purpose:
# purpose = ks[0]['purpose']
# if ks and not encoding:
# encoding = ks[0]['encoding']
# key_path = ks[0]['key_path']
key_path, purpose, encoding = get_key_structure_data(witness_type, multisig, purpose, encoding)
else:
if purpose is None:
purpose = 0
Expand Down Expand Up @@ -1568,20 +1571,20 @@ def import_master_key(self, hdkey, name='Masterkey (imported)'):
if (self.main_key.depth != 1 and self.main_key.depth != 3 and self.main_key.depth != 4) or \
self.main_key.key_type != 'bip32':
raise WalletError("Current main key is not a valid BIP32 public master key")
# pm = self.public_master()
if not (self.network.name == self.main_key.network.name == hdkey.network.name):
raise WalletError("Network of Wallet class, main account key and the imported private key must use "
"the same network")
if self.main_key.wif != hdkey.public_master().wif():
raise WalletError("This key does not correspond to current public master key")

hdkey.key_type = 'bip32'
ks = [k for k in WALLET_KEY_STRUCTURES if
k['witness_type'] == self.witness_type and k['multisig'] == self.multisig and k['purpose'] is not None]
if len(ks) > 1:
raise WalletError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for "
"witness_type - multisig combination")
self.key_path = ks[0]['key_path']
# ks = [k for k in WALLET_KEY_STRUCTURES if
# k['witness_type'] == self.witness_type and k['multisig'] == self.multisig and k['purpose'] is not None]
# if len(ks) > 1:
# raise WalletError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for "
# "witness_type - multisig combination")
# self.key_path = ks[0]['key_path']
self.key_path, _, _ = get_key_structure_data(self.witness_type, self.multisig)
self.main_key = WalletKey.from_key(
key=hdkey, name=name, session=self._session, wallet_id=self.wallet_id, network=network,
account_id=account_id, purpose=self.purpose, key_type='bip32', witness_type=self.witness_type)
Expand All @@ -1599,7 +1602,7 @@ def import_master_key(self, hdkey, name='Masterkey (imported)'):
self._commit()
return self.main_key

def import_key(self, key, account_id=0, name='', network=None, purpose=44, key_type=None):
def import_key(self, key, account_id=0, name='', network=None, purpose=84, key_type=None):
"""
Add new single key to wallet.
Expand All @@ -1611,7 +1614,7 @@ def import_key(self, key, account_id=0, name='', network=None, purpose=44, key_t
:type name: str
:param network: Network name, method will try to extract from key if not specified. Raises warning if network could not be detected
:type network: str
:param purpose: BIP definition used, default is BIP44
:param purpose: BIP44 definition used, default is 84 (segwit)
:type purpose: int
:param key_type: Key type of imported key, can be single. Unrelated to wallet, bip32, bip44 or master for new or extra master key import. Default is 'single'
:type key_type: str
Expand Down Expand Up @@ -1707,7 +1710,7 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id,
name=name[:80], wallet_id=self.wallet_id, purpose=self.purpose, account_id=account_id,
depth=depth, change=change, address_index=address_index, parent_id=0, is_private=False, path=path,
public=address.hash_bytes, wif='multisig-%s' % address, address=address.address, cosigner_id=cosigner_id,
key_type='multisig', network_name=network)
key_type='multisig', witness_type=self.witness_type, network_name=network)
self._session.add(multisig_key)
self._commit()
for child_id in public_key_ids:
Expand All @@ -1716,7 +1719,7 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id,
self._commit()
return self.key(multisig_key.id)

def new_key(self, name='', account_id=None, change=0, cosigner_id=None, network=None):
def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_type=None, network=None):
"""
Create a new HD Key derived from this wallet's masterkey. An account will be created for this wallet
with index 0 if there is no account defined yet.
Expand Down Expand Up @@ -1746,20 +1749,23 @@ def new_key(self, name='', account_id=None, change=0, cosigner_id=None, network=
if network != self.network.name and "coin_type'" not in self.key_path:
raise WalletError("Multiple networks not supported by wallet key structure")
if self.multisig:
# if witness_type:
# TODO: raise error
if not self.multisig_n_required:
raise WalletError("Multisig_n_required not set, cannot create new key")
if cosigner_id is None:
if self.cosigner_id is None:
raise WalletError("Missing Cosigner ID value, cannot create new key")
cosigner_id = self.cosigner_id
witness_type = self.witness_type if not witness_type else witness_type

address_index = 0
if self.multisig and cosigner_id is not None and (len(self.cosigner) > cosigner_id and self.cosigner[cosigner_id].key_path == 'm' or self.cosigner[cosigner_id].key_path == ['m']):
req_path = []
else:
prevkey = self._session.query(DbKey).\
filter_by(wallet_id=self.wallet_id, purpose=self.purpose, network_name=network, account_id=account_id,
change=change, cosigner_id=cosigner_id, depth=self.key_depth).\
witness_type=witness_type, change=change, cosigner_id=cosigner_id, depth=self.key_depth).\
order_by(DbKey.address_index.desc()).first()
if prevkey:
address_index = prevkey.address_index + 1
Expand Down Expand Up @@ -2090,7 +2096,7 @@ def path_expand(self, path, level_offset=None, account_id=None, cosigner_id=0, a
witness_type=self.witness_type, network=network)

def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosigner_id=None,
address_index=0, change=0, network=None, recreate=False):
address_index=0, change=0, witness_type=None, network=None, recreate=False):
"""
Return key for specified path. Derive all wallet keys in path if they not already exists
Expand Down Expand Up @@ -2141,11 +2147,17 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi
level_offset_key = level_offset - self.main_key.depth

key_path = self.key_path
witness_type = witness_type if witness_type else self.witness_type
purpose = self.purpose
encoding = self.encoding
# if witness_type != self.witness_type:
# purpose = 44
# encoding = 'base58' if witness_type == 'legacy' else 'bech32'
if self.multisig and cosigner_id is not None and len(self.cosigner) > cosigner_id:
key_path = self.cosigner[cosigner_id].key_path
fullpath = path_expand(path, key_path, level_offset_key, account_id=account_id, cosigner_id=cosigner_id,
purpose=self.purpose, address_index=address_index, change=change,
witness_type=self.witness_type, network=network)
purpose=purpose, address_index=address_index, change=change,
witness_type=witness_type, network=network)

if self.multisig and self.cosigner:
public_keys = []
Expand All @@ -2158,7 +2170,7 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi
public_keys.append(wk)
return self._new_key_multisig(public_keys, name, account_id, change, cosigner_id, network, address_index)

# Check for closest ancestor in wallet\
# Check for closest ancestor in wallet
wpath = fullpath
if self.main_key.depth and fullpath and fullpath[0] != 'M':
wpath = ["M"] + fullpath[self.main_key.depth + 1:]
Expand All @@ -2183,6 +2195,8 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi
nk = None
parent_id = topkey.key_id
ck = topkey.key()
ck.witness_type = witness_type
ck.encoding = encoding
newpath = topkey.path
n_items = len(str(dbkey.path).split('/'))
for lvl in fullpath[n_items:]:
Expand All @@ -2200,8 +2214,8 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi
key_name = "%s %s" % (self.key_path[len(newpath.split('/'))-1], lvl)
key_name = key_name.replace("'", "").replace("_", " ")
nk = WalletKey.from_key(key=ck, name=key_name, wallet_id=self.wallet_id, account_id=account_id,
change=change, purpose=self.purpose, path=newpath, parent_id=parent_id,
encoding=self.encoding, witness_type=self.witness_type,
change=change, purpose=purpose, path=newpath, parent_id=parent_id,
encoding=encoding, witness_type=witness_type,
cosigner_id=cosigner_id, network=network, session=self._session)
self._key_objects.update({nk.key_id: nk})
parent_id = nk.key_id
Expand Down

0 comments on commit 33c2ed7

Please sign in to comment.