ckcc-protocol/ckcc/electrum.py

157 lines
5.5 KiB
Python

# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
import re
import os
import copy
import json
from ckcc.utils import xfp2str
from ckcc.client import ColdcardDevice
from ckcc.protocol import CCProtocolPacker
MULTISIG_WALLET_TYPE_PATTERN = r"^\d+of\d+$"
MULTISIG_WALLET_KEY_PATTERN = r"^x\d+/$"
def is_hww_keystore(keystore: dict) -> bool:
return keystore["type"] == "hardware"
def is_multisig_wallet(wallet: dict) -> bool:
if re.match(MULTISIG_WALLET_TYPE_PATTERN, wallet["wallet_type"]):
return True
return False
def is_multisig_wallet_key(key: str) -> bool:
if re.match(MULTISIG_WALLET_KEY_PATTERN, key):
return True
return False
def collect_multisig_hww_keystores_from_wallet(wallet: dict) -> dict:
"""Find all hardware keystore objects in multisig wallet dict"""
if not is_multisig_wallet(wallet):
raise RuntimeError("Not an electrum multisig wallet")
return {
key: value
for key, value in wallet.items()
if is_multisig_wallet_key(key)
if is_hww_keystore(value)
}
def multisig_find_target(keystores: dict, key: str, value: str) -> tuple:
"""Find target keystore in list of keystores by key equals value"""
result = [
(k, keystore)
for k, keystore in keystores.items()
if keystore.get(key, None) == value
]
length = len(result)
if length != 1:
# if this is true, we have found more than one keystore and therefore
# key value pair is ambiguous
msg = "Found {} keystores.".format(length)
raise RuntimeError(
msg if length == 0 else msg + "{}:{} is ambiguous".format(key, value)
)
return result[0]
def filepath_append_cc(f_path: str) -> str:
"""Append '_cc' suffix to file path. Do consider one file extension"""
dirname = os.path.dirname(f_path)
filename, file_ext = os.path.splitext(os.path.basename(f_path))
result = os.path.join(dirname, "{}_cc".format(filename) + file_ext)
return result
def cc_adjust_hww_keystore(keystore: dict, dev: ColdcardDevice = None) -> dict:
"""Create new updated version of keystore"""
new_keystore = copy.deepcopy(keystore)
if not is_hww_keystore(keystore):
raise RuntimeError("Not a hardware wallet type")
# 1-3 can be done without coldcard connected
#
# 1. change hw type to coldcard
new_keystore["hw_type"] = "coldcard"
# 2. soft device id should be nullified
new_keystore["soft_device_id"] = None
# 3. remove cfg key if exists (ledger specific)
if "cfg" in new_keystore:
del new_keystore["cfg"]
# 4. label ? we can do something about it - at least remove the label that is no longer in use
new_keystore["label"] = "Coldcard {}".format(new_keystore["root_fingerprint"])
# for next steps we need coldcard connected (unnecessary)
if dev:
# 4. label Coldcard + fingerprint
xfp = dev.master_fingerprint
xfp = xfp2str(xfp).lower() # if any letters - lower them
if xfp != new_keystore["root_fingerprint"]:
raise RuntimeError(
"Fingerprint missmatch! Is this a correct coldcard/wallet file?"
" Make sure that your bip39 passphrase is in effect (if used)."
" device fingerprint {}; wallet fingerprint {}".format(xfp, new_keystore["root_fingerprint"])
)
label = "Coldcard {}".format(xfp)
new_keystore["label"] = label
# 5. ckcc xpub (master xpub)
master_ext_pubkey = dev.master_xpub
new_keystore["ckcc_xpub"] = master_ext_pubkey
return new_keystore
def cc_adjust_multisig_hww_keystore(wallet: dict, key: str, value: str, dev: ColdcardDevice = None) -> dict:
"""Update multisig wallet keystore"""
# 1. find target keystore
k, keystore = multisig_find_target(
keystores=collect_multisig_hww_keystores_from_wallet(wallet),
key=key,
value=value,
)
# 2. create new adjusted keystore for target
new_keystore = cc_adjust_hww_keystore(keystore, dev)
# 3. update target keystore in wallet
wallet[k] = new_keystore
return wallet
def convert2cc(wallet_str: str, dev: ColdcardDevice = None, key: str = None, val: str = None):
wallet = json.loads(wallet_str)
wallet_type = wallet["wallet_type"]
if wallet_type == "standard":
new_keystore = cc_adjust_hww_keystore(wallet["keystore"], dev)
wallet["keystore"] = new_keystore
elif is_multisig_wallet(wallet):
if key is None and val is None and dev is None:
# dev is not defined, key val is not defined, we are in multisig - have to fail
raise RuntimeError("--key and --val have to be specified for multisig wallets")
elif key is None and val is None and dev:
# user haven't provided arguments - try some auto-magic if coldcard is connected
# look for root fingerprint
cc_adjust_multisig_hww_keystore(
wallet,
key="root_fingerprint",
value=xfp2str(dev.master_fingerprint).lower(),
dev=dev
)
# is it sufficient to just check xfp?
# shouldn't I try to re-create xpub (Vpub) or whatever I get as derivation path?
else:
# key val specified
cc_adjust_multisig_hww_keystore(
wallet,
key=key,
value=val,
dev=dev
)
else:
raise RuntimeError("Unsupported wallet type: {}".format(wallet_type))
return json.dumps(wallet)