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")
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
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
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
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']}) """)
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, }
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"
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"
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." )
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." )
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", {}))
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)
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
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, )