diff --git a/src/cryptoadvance/specter/liquid/wallet.py b/src/cryptoadvance/specter/liquid/wallet.py index bcdb57bcf9..f6f00e3775 100644 --- a/src/cryptoadvance/specter/liquid/wallet.py +++ b/src/cryptoadvance/specter/liquid/wallet.py @@ -1,5 +1,6 @@ from ..wallet import * from ..addresslist import Address +from embit import ec from embit.liquid.pset import PSET from embit.liquid.transaction import LTransaction @@ -7,6 +8,143 @@ class LWallet(Wallet): MIN_FEE_RATE = 0.1 + @classmethod + def create( + cls, + rpc, + rpc_path, + working_folder, + device_manager, + wallet_manager, + name, + alias, + sigs_required, + key_type, + keys, + devices, + core_version=None, + ): + """Creates a wallet. If core_version is not specified - get it from rpc""" + # get xpubs in a form [fgp/der]xpub from all keys + xpubs = [key.metadata["combined"] for key in keys] + recv_keys = ["%s/0/*" % xpub for xpub in xpubs] + change_keys = ["%s/1/*" % xpub for xpub in xpubs] + is_multisig = len(keys) > 1 + # we start by constructing an argument for descriptor wrappers + if is_multisig: + recv_descriptor = "sortedmulti({},{})".format( + sigs_required, ",".join(recv_keys) + ) + change_descriptor = "sortedmulti({},{})".format( + sigs_required, ",".join(change_keys) + ) + else: + recv_descriptor = recv_keys[0] + change_descriptor = change_keys[0] + # now we iterate over script-type in reverse order + # to get sh(wpkh(xpub)) from sh-wpkh and xpub + arr = key_type.split("-") + for el in arr[::-1]: + recv_descriptor = "%s(%s)" % (el, recv_descriptor) + change_descriptor = "%s(%s)" % (el, change_descriptor) + + # get blinding key for the wallet + blinding_key = None + if len(devices) == 1: + blinding_key = devices[0].blinding_key + # if we don't have slip77 key for a device or it is multisig + # we use chaincodes to generate slip77 key. + if not blinding_key: + desc = LDescriptor.from_string(recv_descriptor) + # For now we use sha256(b"blinding_key", xor(chaincodes)) as a blinding key + # where chaincodes are corresponding to xpub of the first receiving address. + # It's not a standard but we use that until musig(blinding_xpubs) is implemented. + # Chaincodes of the first address are not used anywhere else so they can be used + # as a source for the blinding keys. They are also independent of the xpub's origin. + xor = bytearray(32) + desc_keys = desc.derive(0).keys + for k in desc_keys: + if k.is_extended: + chaincode = k.key.chain_code + for i in range(32): + xor[i] = xor[i] ^ chaincode[i] + secret = hashlib.sha256(b"blinding_key" + bytes(xor)).digest() + blinding_key = ec.PrivateKey(secret).wif() + if blinding_key: + recv_descriptor = f"blinded(slip77({blinding_key}),{recv_descriptor})" + change_descriptor = f"blinded(slip77({blinding_key}),{change_descriptor})" + + recv_descriptor = AddChecksum(recv_descriptor) + change_descriptor = AddChecksum(change_descriptor) + assert recv_descriptor != change_descriptor + + # get Core version if we don't know it + if core_version is None: + core_version = rpc.getnetworkinfo().get("version", 0) + + use_descriptors = core_version >= 209900 + # v20.99 is pre-v21 Elements Core for descriptors + if use_descriptors: + # Use descriptor wallet + rpc.createwallet(os.path.join(rpc_path, alias), True, True, "", False, True) + else: + rpc.createwallet(os.path.join(rpc_path, alias), True) + + wallet_rpc = rpc.wallet(os.path.join(rpc_path, alias)) + # import descriptors + args = [ + { + "desc": desc, + "internal": change, + "timestamp": "now", + "watchonly": True, + } + for (change, desc) in [(False, recv_descriptor), (True, change_descriptor)] + ] + for arg in args: + if use_descriptors: + arg["active"] = True + else: + arg["keypool"] = True + arg["range"] = [0, cls.GAP_LIMIT] + + assert args[0] != args[1] + + # Descriptor wallets were introduced in v0.21.0, but upgraded nodes may + # still have legacy wallets. Use getwalletinfo to check the wallet type. + # The "keypool" for descriptor wallets is automatically refilled + if use_descriptors: + res = wallet_rpc.importdescriptors(args) + else: + res = wallet_rpc.importmulti(args, {"rescan": False}) + + assert all([r["success"] for r in res]) + + return cls( + name, + alias, + "{} of {} {}".format(sigs_required, len(keys), purposes[key_type]) + if len(keys) > 1 + else purposes[key_type], + addrtypes[key_type], + "", + -1, + "", + -1, + 0, + 0, + recv_descriptor, + change_descriptor, + keys, + devices, + sigs_required, + {}, + [], + os.path.join(working_folder, "%s.json" % alias), + device_manager, + wallet_manager, + ) + def get_balance(self): try: full_balance = ( diff --git a/src/cryptoadvance/specter/managers/wallet_manager.py b/src/cryptoadvance/specter/managers/wallet_manager.py index dda5cc45aa..330b7b102c 100644 --- a/src/cryptoadvance/specter/managers/wallet_manager.py +++ b/src/cryptoadvance/specter/managers/wallet_manager.py @@ -14,7 +14,7 @@ from ..rpc import RpcError, get_default_datadir from ..specter_error import SpecterError from ..util.descriptor import AddChecksum -from ..wallet import Wallet +from ..wallet import Wallet, purposes from ..liquid.wallet import LWallet from embit import ec @@ -34,27 +34,6 @@ logger = logging.getLogger(__name__) -purposes = OrderedDict( - { - None: "General", - "wpkh": "Single (Segwit)", - "sh-wpkh": "Single (Nested)", - "pkh": "Single (Legacy)", - "wsh": "Multisig (Segwit)", - "sh-wsh": "Multisig (Nested)", - "sh": "Multisig (Legacy)", - } -) - -addrtypes = { - "pkh": "legacy", - "sh-wpkh": "p2sh-segwit", - "wpkh": "bech32", - "sh": "legacy", - "sh-wsh": "p2sh-segwit", - "wsh": "bech32", -} - class WalletManager: # chain is required to manage wallets when bitcoind is not running @@ -322,82 +301,19 @@ def create_wallet(self, name, sigs_required, key_type, keys, devices): wallet_alias = alias("%s %d" % (name, i)) i += 1 - arr = key_type.split("-") - descs = [key.metadata["combined"] for key in keys] - recv_descs = ["%s/0/*" % desc for desc in descs] - change_descs = ["%s/1/*" % desc for desc in descs] - if len(keys) > 1: - recv_descriptor = "sortedmulti({},{})".format( - sigs_required, ",".join(recv_descs) - ) - change_descriptor = "sortedmulti({},{})".format( - sigs_required, ",".join(change_descs) - ) - else: - recv_descriptor = recv_descs[0] - change_descriptor = change_descs[0] - for el in arr[::-1]: - recv_descriptor = "%s(%s)" % (el, recv_descriptor) - change_descriptor = "%s(%s)" % (el, change_descriptor) - - if is_liquid(self.chain): - blinding_key = None - if len(devices) == 1: - blinding_key = devices[0].blinding_key - if not blinding_key: - desc = Descriptor.from_string(recv_descriptor.split("#")[0]) - # For now we use sha256(b"blinding_key", xor(chaincodes)) as a blinding key - # where chaincodes are corresponding to xpub of the first receiving address - xor = bytearray(32) - desc_keys = desc.derive(0).keys - for k in desc_keys: - if k.is_extended: - chaincode = k.key.chain_code - for i in range(32): - xor[i] = xor[i] ^ chaincode[i] - secret = hashlib.sha256(b"blinding_key" + bytes(xor)).digest() - blinding_key = ec.PrivateKey(secret).wif() - if blinding_key: - recv_descriptor = f"blinded(slip77({blinding_key}),{recv_descriptor})" - change_descriptor = ( - f"blinded(slip77({blinding_key}),{change_descriptor})" - ) - - recv_descriptor = AddChecksum(recv_descriptor) - change_descriptor = AddChecksum(change_descriptor) - - # v20.99 is pre-v21 Elements Core for descriptors - if self.bitcoin_core_version_raw >= 209900: - # Use descriptor wallet - self.rpc.createwallet( - os.path.join(self.rpc_path, wallet_alias), True, True, "", False, True - ) - else: - self.rpc.createwallet(os.path.join(self.rpc_path, wallet_alias), True) - - w = self.WalletClass( + w = self.WalletClass.create( + self.rpc, + self.rpc_path, + self.working_folder, + self.device_manager, + self, name, wallet_alias, - "{} of {} {}".format(sigs_required, len(keys), purposes[key_type]) - if len(keys) > 1 - else purposes[key_type], - addrtypes[key_type], - "", - -1, - "", - -1, - 0, - 0, - recv_descriptor, - change_descriptor, + sigs_required, + key_type, keys, devices, - sigs_required, - {}, - [], - os.path.join(self.working_folder, "%s.json" % wallet_alias), - self.device_manager, - self, + self.bitcoin_core_version_raw, ) # save wallet file to disk if w and self.working_folder is not None: diff --git a/src/cryptoadvance/specter/node.py b/src/cryptoadvance/specter/node.py index 5ed7929769..9de1b6ceb2 100644 --- a/src/cryptoadvance/specter/node.py +++ b/src/cryptoadvance/specter/node.py @@ -319,7 +319,7 @@ def test_rpc(self): r["code"] = 0 try: r["tests"]["recent_version"] = ( - int(rpc.getnetworkinfo()["version"]) >= 170000 + int(rpc.getnetworkinfo()["version"]) >= 200000 ) if not r["tests"]["recent_version"]: r["err"] = "Core Node might be too old" diff --git a/src/cryptoadvance/specter/templates/includes/sidebar/sidebar.jinja b/src/cryptoadvance/specter/templates/includes/sidebar/sidebar.jinja index 2b4f7ea8de..0e83ef7ef5 100644 --- a/src/cryptoadvance/specter/templates/includes/sidebar/sidebar.jinja +++ b/src/cryptoadvance/specter/templates/includes/sidebar/sidebar.jinja @@ -42,8 +42,8 @@ {% if specter.info["initialblockdownload"] %}
Bitcoin Core is still syncing...
(data might be outdated)
Bitcoin Core version is outdated.
Some features might not work...
(minimum required: v19.0.0).
Bitcoin Core version is outdated.
Some features might not work...
(minimum required: v20.0.0).