Exemplo n.º 1
0
 def create_psbt(self, wallet):
     """creates the PSBT via the wallet and modifies it for if substract is true
     If there was a "estimate_fee" in the request_form, the PSBT will not get persisted
     """
     try:
         self.psbt = wallet.createpsbt(self.addresses, self.amounts,
                                       **self.kwargs)
         if self.psbt is None:
             raise SpecterError(
                 "Probably you don't have enough funds, or something else..."
             )
         else:
             # calculate new amount if we need to subtract
             if self.kwargs["subtract"]:
                 for v in self.psbt["tx"]["vout"]:
                     if self.addresses[0] in v["scriptPubKey"].get(
                             "addresses",
                         [""
                          ]) or self.addresses[0] == v["scriptPubKey"].get(
                              "address", ""):
                         self.amounts[0] = v["value"]
         return self.psbt
     except Exception as e:
         logger.exception(e)
         raise SpecterError(f"{e} ... check the logs for the stacktrace")
Exemplo n.º 2
0
 def rescan_as_needed(self, specter):
     """will rescan the created wallet"""
     if not hasattr(self, "wallet"):
         raise Exception("called rescan_as_needed before create_wallet")
     potential_errors = None
     try:
         # get min of the two
         # if the node is still syncing
         # and the first block with tx is not there yet
         startblock = min(
             self.wallet_data.get("blockheight", specter.info.get("blocks", 0)),
             specter.info.get("blocks", 0),
         )
         # check if pruned
         if specter.info.get("pruned", False):
             newstartblock = max(startblock, specter.info.get("pruneheight", 0))
             if newstartblock > startblock:
                 potential_errors = SpecterError(
                     f"Using pruned node - we will only rescan from block {newstartblock}"
                 )
                 startblock = newstartblock
         self.wallet.rpc.rescanblockchain(startblock, no_wait=True)
         logger.info("Rescanning Blockchain ...")
     except Exception as e:
         logger.error("Exception while rescanning blockchain: %r" % e)
         if potential_errors:
             potential_errors = SpecterError(
                 str(potential_errors)
                 + " and "
                 + "Failed to perform rescan for wallet: %r" % e
             )
     self.wallet.getdata()
     if potential_errors:
         raise potential_errors
Exemplo n.º 3
0
def compare(version1: str, version2: str) -> int:
    """Compares two version strings like v1.5.1 and v1.6.0 and returns
    * 1 : version2 is bigger that version1
    * -1 : version1 is bigger than version2
    * 0 : both are the same
    This is not supporting semver and it doesn't take any postfix (-pre5)
    into account and is therefore a naive implementation
    """
    version1 = _parse_version(version1)
    version2 = _parse_version(version2)

    if version1["postfix"] != "" or version2["postfix"] != "":
        raise SpecterError(
            f"Cannot compare if either version has a postfix : {version1} and {version2}"
        )
    if version1["major"] > version2["major"]:
        return -1
    elif version1["major"] < version2["major"]:
        return 1
    if version1["minor"] > version2["minor"]:
        return -1
    elif version1["minor"] < version2["minor"]:
        return 1
    if version1["patch"] > version2["patch"]:
        return -1
    elif version1["patch"] < version2["patch"]:
        return 1
    return 0
Exemplo n.º 4
0
    def create_wallet(self, wallet_manager):
        """creates the wallet. Assumes all devices are there (create with create_nonexisting_signers)
        will also keypoolrefill and import_labels
        """
        try:
            kwargs = {}
            if (
                isinstance(self.descriptor, LDescriptor)
                and self.descriptor.blinding_key
            ):
                kwargs["blinding_key"] = self.descriptor.blinding_key.key

            self.wallet = wallet_manager.create_wallet(
                name=self.wallet_name,
                sigs_required=self.sigs_required,
                key_type=self.address_type,
                keys=self.keys,
                devices=self.cosigners,
                **kwargs,
            )
        except Exception as e:
            raise SpecterError(f"Failed to create wallet: {e}")
        logger.info(f"Created Wallet {self.wallet}")
        self.wallet.keypoolrefill(0, self.wallet.IMPORT_KEYPOOL, change=False)
        self.wallet.keypoolrefill(0, self.wallet.IMPORT_KEYPOOL, change=True)
        self.wallet.import_labels(self.wallet_data.get("labels", {}))
        return self.wallet
Exemplo n.º 5
0
    def register_blueprint_for_ext(cls, clazz, ext):
        if not clazz.has_blueprint:
            return
        if hasattr(clazz, "blueprint_module"):
            import_name = clazz.blueprint_module
            controller_module = clazz.blueprint_module
        else:
            # The import_name helps to locate the root_path for the blueprint
            import_name = f"cryptoadvance.specter.services.{clazz.id}.service"
            controller_module = f"cryptoadvance.specter.services.{clazz.id}.controller"

        clazz.blueprint = Blueprint(
            f"{clazz.id}_endpoint",
            import_name,
            template_folder=get_template_static_folder("templates"),
            static_folder=get_template_static_folder("static"),
        )

        def inject_stuff():
            """Can be used in all jinja2 templates"""
            return dict(specter=app.specter, service=ext)

        clazz.blueprint.context_processor(inject_stuff)

        # Import the controller for this service
        logger.info(f"  Loading Controller {controller_module}")
        controller_module = import_module(controller_module)

        # finally register the blueprint
        if clazz.isolated_client:
            ext_prefix = app.config["ISOLATED_CLIENT_EXT_URL_PREFIX"]
        else:
            ext_prefix = app.config["EXT_URL_PREFIX"]

        try:
            app.register_blueprint(clazz.blueprint,
                                   url_prefix=f"{ext_prefix}/{clazz.id}")
            logger.info(f"  Mounted {clazz.id} to {ext_prefix}/{clazz.id}")
            if (app.testing and len([
                    vf for vf in app.view_functions if vf.startswith(clazz.id)
            ]) <= 1):  # the swan-static one
                # Yet again that nasty workaround which has been described in the archblog.
                # The easy variant can be found in server.py
                # The good news is, that we'll only do that for testing
                import importlib

                logger.info("Reloading Extension controller")
                importlib.reload(controller_module)
                app.register_blueprint(clazz.blueprint,
                                       url_prefix=f"{ext_prefix}/{clazz.id}")
        except AssertionError as e:
            if str(e).startswith("A name collision"):
                raise SpecterError(f"""
                There is a name collision for the {clazz.blueprint.name}. \n
                This is probably because you're running in DevelopementConfig and configured
                the extension at the same time in the EXTENSION_LIST which currently loks like this:
                {app.config['EXTENSION_LIST']})
                """)
Exemplo n.º 6
0
def _parse_version(version: str) -> dict:
    """Parses version-strings like v1.5.6-pre5 and returns a dict"""
    if version[0] != "v":
        raise SpecterError(f"version {version} does not have a preceding 'v'")
    version = version[1:]
    version_ar = version.split(".")
    if len(version_ar) != 3:
        raise SpecterError(
            f"version {version} does not have 3 separated digits")
    postfix = ""
    if "-" in version_ar[2]:
        postfix = version_ar[2].split("-")[1]
        version_ar[2] = version_ar[2].split("-")[0]
    return {
        "major": int(version_ar[0]),
        "minor": int(version_ar[1]),
        "patch": int(version_ar[2]),
        "postfix": postfix,
    }
Exemplo n.º 7
0
 def __init__(self, wallet_json, specter):
     """this will analyze the wallet_json and specifies self. ...:
     * wallet_name
     * recv_descriptor
     * cosigners_types
     from recv_descriptor and specter.chain:
     * descriptor
     from descriptor:
     * keys
     * cosigners
     * unknown_cosigners
     * unknown_cosigners_types
     """
     try:
         self.wallet_data = json.loads(wallet_json)
         (
             self.wallet_name,
             self.recv_descriptor,
             self.cosigners_types,
         ) = WalletImporter.parse_wallet_data_import(self.wallet_data)
     except Exception as e:
         logger.warning(f"Trying to import: {wallet_json}")
         raise SpecterError(f"Unsupported wallet import format:{e}")
     try:
         self.descriptor = Descriptor.parse(
             AddChecksum(self.recv_descriptor.split("#")[0]),
             testnet=is_testnet(specter.chain),
         )
         if self.descriptor is None:
             raise SpecterError(
                 f"Invalid wallet descriptor. (returns None)")
     except Exception as e:
         raise SpecterError(f"Invalid wallet descriptor: {e}")
     if self.wallet_name in specter.wallet_manager.wallets_names:
         raise SpecterError(f"Wallet with the same name already exists")
     (
         self.keys,
         self.cosigners,
         self.unknown_cosigners,
         self.unknown_cosigners_types,
     ) = self.descriptor.parse_signers(specter.device_manager.devices,
                                       self.cosigners_types)
     self.wallet_type = "multisig" if self.descriptor.multisig_N > 1 else "simple"
Exemplo n.º 8
0
 def __init__(self, wallet_json, specter, device_manager=None):
     """this will analyze the wallet_json and specifies self. ...:
     * wallet_name
     * recv_descriptor
     * cosigners_types
     from recv_descriptor and specter.chain:
     * descriptor
     from descriptor:
     * keys
     * cosigners
     * unknown_cosigners
     * unknown_cosigners_types
     """
     DescriptorCls = LDescriptor if specter.is_liquid else Descriptor
     if device_manager is None:
         device_manager = specter.device_manager
     try:
         self.wallet_data = json.loads(wallet_json)
         (
             self.wallet_name,
             recv_descriptor,
             self.cosigners_types,
         ) = WalletImporter.parse_wallet_data_import(self.wallet_data)
     except Exception as e:
         logger.warning(f"Trying to import: {wallet_json}")
         raise SpecterError(f"Unsupported wallet import format:{e}")
     try:
         self.descriptor = DescriptorCls.from_string(recv_descriptor)
         self.check_descriptor()
     except Exception as e:
         raise SpecterError(f"Invalid wallet descriptor: {e}")
     if self.wallet_name in specter.wallet_manager.wallets_names:
         raise SpecterError(f"Wallet with the same name already exists")
     (
         self.keys,
         self.cosigners,
         self.unknown_cosigners,
         self.unknown_cosigners_types,
     ) = self.parse_signers(device_manager.devices, self.cosigners_types)
     self.wallet_type = "multisig" if self.descriptor.is_basic_multisig else "simple"
Exemplo n.º 9
0
    def check_descriptor(self):
        # Sparrow fix: if all keys have None as allowed derivation - set allowed derivation to [0, None]
        if all([
                k.allowed_derivation is None
                or k.allowed_derivation.indexes == []
                for k in self.descriptor.keys if k.is_extended
        ]):
            for k in self.descriptor.keys:
                if k.is_extended:
                    k.allowed_derivation = AllowedDerivation([0, None])

        # Check that all keys are HD keys and all have default derivation
        for key in self.descriptor.keys:
            if not key.is_extended:
                raise SpecterError("Only HD keys are supported in descriptor")
            if key.allowed_derivation is None or key.allowed_derivation.indexes != [
                    0,
                    None,
            ]:
                raise SpecterError(
                    "Descriptor key has wrong derivation, only /0/* derivation is supported."
                )
Exemplo n.º 10
0
 def _find_appropriate_name(self):
     if not os.path.isdir(os.path.join(self.data_folder, "nodes")):
         return "specter_bitcoin"
     if not os.path.isdir(
             os.path.join(self.data_folder, "nodes", "specter_bitcoin")):
         return "specter_bitcoin"
     # Hmm, now it gets a bit trieckier
     if not os.path.isdir(
             os.path.join(self.data_folder, "nodes", "specter_migrated")):
         return "specter_migrated"
     # Now it's getting fishy
     raise SpecterError(
         "I found a node called 'specter_migrated'. This migration script should not run twice."
     )
Exemplo n.º 11
0
 def create_wallet(self, wallet_manager):
     """creates the wallet. Assumes all devices are there (create with create_nonexisting_signers)
     will also keypoolrefill and import_labels
     """
     try:
         self.wallet = wallet_manager.create_wallet(
             name=self.wallet_name,
             sigs_required=self.descriptor.multisig_M,
             key_type=self.descriptor.address_type,
             keys=self.keys,
             devices=self.cosigners,
         )
     except Exception as e:
         raise SpecterError(f"Failed to create wallet: {e}")
     self.wallet.keypoolrefill(0, self.wallet.IMPORT_KEYPOOL, change=False)
     self.wallet.keypoolrefill(0, self.wallet.IMPORT_KEYPOOL, change=True)
     self.wallet.import_labels(self.wallet_data.get("labels", {}))
Exemplo n.º 12
0
 def execute(self):
     source_folder = os.path.join(self.data_folder, ".bitcoin")
     if not os.path.isdir(source_folder):
         logger.info(
             "No .bitcoin directory found in {self.data_folder}. Nothing to do"
         )
         return
     if not os.path.isdir(os.path.join(self.data_folder,
                                       "bitcoin-binaries")):
         raise SpecterError(
             "Could not proceed with migration as bitcoin-binaries are not existing."
         )
     if not self._check_port_free():
         logger.error(
             "There is already a Node with the default port configured or running. Won't migrate!"
         )
         return
     # The version will be the version shipped with specter
     bitcoin_version = BaseConfig.INTERNAL_BITCOIND_VERSION
     logger.info(
         f".bitcoin directory detected in {self.data_folder}. Migrating ..."
     )
     recommended_name = self._find_appropriate_name()
     target_folder = os.path.join(self.data_folder, "nodes",
                                  recommended_name)
     logger.info(f"Migrating to folder {target_folder}")
     os.makedirs(target_folder)
     logger.info(f"Moving .bitcoin to folder {target_folder}")
     shutil.move(source_folder, os.path.join(target_folder,
                                             ".bitcoin-main"))
     if os.path.isdir(os.path.join(source_folder, "bitcoin.conf")):
         logger.info("Removing bitcoin.conf file")
         os.remove(os.path.join(source_folder, "bitcoin.conf"))
     definition_file = os.path.join(target_folder, "specter_bitcoin.json")
     logger.info(
         f"Creating {definition_file}. This will cause some warnings and even errors about not being able to connect to the node which can be ignored."
     )
     nm = NodeManager(
         data_folder=os.path.join(self.data_folder, "nodes"),
         bitcoind_path=os.path.join(self.data_folder, "bitcoin-binaries",
                                    "bin", "bitcoind"),
         internal_bitcoind_version=bitcoin_version,
     )
     # Should create a json (see fullpath) like the one below:
     node = nm.add_internal_node(recommended_name)
Exemplo n.º 13
0
 def paymentinfo_from_text(cls, specter, wallet, recipients_txt,
                           recipients_amount_unit):
     """calculates the correct format needed by wallet.createpsbt() out of a request-form
     out of a textbox holding addresses and amounts.
     """
     i = 0
     addresses = []
     labels = []
     amounts = []
     amount_units = []
     for output in recipients_txt.splitlines():
         addresses.append(output.split(",")[0].strip())
         if recipients_amount_unit == "sat":
             amounts.append(float(output.split(",")[1].strip()) / 1e8)
         elif recipients_amount_unit == "btc":
             amounts.append(float(output.split(",")[1].strip()))
         else:
             raise SpecterError(
                 f"Unknown recipients_amount_unit: {recipients_amount_unit}"
             )
         labels.append("")
     return addresses, labels, amounts, amount_units
Exemplo n.º 14
0
    def parse(cls, desc, testnet=False):
        sh_wpkh = None
        wpkh = None
        sh = None
        sh_wsh = None
        wsh = None
        origin_fingerprint = None
        origin_path = None
        base_key_and_path_match = None
        base_key = None
        path_suffix = None
        multisig_M = None
        multisig_N = None
        sort_keys = True

        # Check the checksum
        check_split = desc.split("#")
        # Multiple # in desc
        if len(check_split) > 2:
            raise SpecterError(
                f"Too many separators in the descriptor. Check if there are multiple # in {desc}."
            )
        if len(check_split) == 2:
            # Empty checkusm
            if len(check_split[1]) == 0:
                raise SpecterError("Checksum is empty.")
            # Incorrect length
            elif len(check_split[1]) != 8:
                raise SpecterError(
                    f"Checksum {check_split[1]} doesn't have the correct length. Should be 8 characters not {len(check_split[1])}."
                )
            checksum = DescriptorChecksum(check_split[0])
            # Check of checksum calc
            if checksum.strip() == "":
                raise SpecterError(f"Checksum calculation went wrong.")
            # Wrong checksum
            if checksum != check_split[1]:
                raise SpecterError(
                    f"{check_split[1]} is the wrong checkum should be {checksum}."
                )
        desc = check_split[0]

        if desc.startswith("sh(wpkh("):
            sh_wpkh = True
        elif desc.startswith("wpkh("):
            wpkh = True
        elif desc.startswith("sh(wsh("):
            sh_wsh = True
        elif desc.startswith("wsh("):
            wsh = True
        elif desc.startswith("sh("):
            sh = True

        if sh or sh_wsh or wsh:
            if "multi(" not in desc:
                # only multisig scripts are supported
                return None
            # get the list of keys only
            keys = desc.split(",", 1)[1].split(")", 1)[0].split(",")
            sort_keys = "sortedmulti" in desc
            if "sortedmulti" in desc:
                # sorting makes sense only if individual pubkeys are provided
                base_keys = [
                    x if "]" not in x else x.split("]")[1] for x in keys
                ]
                bare_pubkeys = [
                    k for k in base_keys if k[:2] in ["02", "03", "04"]
                ]
                if len(bare_pubkeys) == len(keys):
                    keys.sort(
                        key=lambda x: x if "]" not in x else x.split("]")[1])
            multisig_M = desc.split(",")[0].split("(")[-1]
            multisig_N = len(keys)
            if int(multisig_M) > multisig_N:
                raise SpecterError(
                    f"Multisig threshold cannot be larger than the number of keys. Threshold is {int(multisig_M)} but only {multisig_N} keys specified."
                )
        else:
            keys = [desc.split("(")[-1].split(")", 1)[0]]

        descriptors = []
        for key in keys:
            origin_fingerprint = None
            origin_path = None
            base_key = None
            path_suffix = None
            origin_match = re.search(r"\[(.*)\]", key)
            if origin_match:
                origin = origin_match.group(1)
                match = re.search(r"^([0-9a-fA-F]{8})(\/.*)", origin)
                if match:
                    origin_fingerprint = match.group(1)
                    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:
                base_key_and_path_match = re.search(r"(\w+)([\d'\/\*]*)", key)

            if base_key_and_path_match:
                base_key = base_key_and_path_match.group(1)
                path_suffix = base_key_and_path_match.group(2)
                if path_suffix == "":
                    path_suffix = None
            else:
                if origin_match is None:
                    return None

            descriptors.append(
                cls(
                    origin_fingerprint,
                    origin_path,
                    base_key,
                    path_suffix,
                    testnet,
                    sh_wpkh,
                    wpkh,
                    sh,
                    sh_wsh,
                    wsh,
                    sort_keys,
                ))

        if len(descriptors) == 1:
            return descriptors[0]
        else:
            # for multisig scripts save as lists all keypaths fields
            return cls(
                [descriptor.origin_fingerprint for descriptor in descriptors],
                [descriptor.origin_path for descriptor in descriptors],
                [descriptor.base_key for descriptor in descriptors],
                [descriptor.path_suffix for descriptor in descriptors],
                testnet,
                sh_wpkh,
                wpkh,
                sh,
                sh_wsh,
                wsh,
                multisig_M,
                multisig_N,
                sort_keys,
            )