diff --git a/contrib/build-linux/appimage/Dockerfile_ub1804 b/contrib/build-linux/appimage/Dockerfile_ub1804 index 524d716f27ea..786631ce72f6 100644 --- a/contrib/build-linux/appimage/Dockerfile_ub1804 +++ b/contrib/build-linux/appimage/Dockerfile_ub1804 @@ -21,7 +21,7 @@ RUN echo deb ${UBUNTU_MIRROR} bionic main restricted universe multiverse > /etc/ libncurses5-dev=6.1-1ubuntu1.18.04 \ libsqlite3-dev=3.22.0-1ubuntu0.4 \ libusb-1.0-0-dev=2:1.0.21-2 \ - libudev-dev=237-3ubuntu10.45 \ + libudev-dev=237-3ubuntu10.47 \ gettext=0.19.8.1-6ubuntu0.3 \ pkg-config=0.29.1-0ubuntu2 \ libdbus-1-3=1.12.2-1ubuntu1.2 \ @@ -34,7 +34,7 @@ RUN echo deb ${UBUNTU_MIRROR} bionic main restricted universe multiverse > /etc/ zlib1g-dev=1:1.2.11.dfsg-0ubuntu2 \ libfreetype6=2.8.1-2ubuntu2.1 \ libfontconfig1=2.12.6-0ubuntu2 \ - libssl-dev=1.1.1-1ubuntu2.1~18.04.8 \ + libssl-dev=1.1.1-1ubuntu2.1~18.04.9 \ rustc=1.47.0+dfsg1+llvm-1ubuntu1~18.04.1 \ && \ rm -rf /var/lib/apt/lists/* && \ diff --git a/electroncash/commands.py b/electroncash/commands.py index d269f6259cc0..a49e35e2ed70 100644 --- a/electroncash/commands.py +++ b/electroncash/commands.py @@ -162,6 +162,16 @@ def EncodeNamedTupleObject(nt): d[k] = DoChk(d[k]) return d + @staticmethod + def address_from_string_check_slp(address, wallet): + addr_str = address + address = Address.from_string(address) + assert not isinstance(address, str) + slp_addr_str = address.to_full_string(Address.FMT_SLPADDR) + if addr_str in slp_addr_str and not wallet.is_slp: + raise BaseException('Cannot check SLP addresses with a non-SLP type wallet.') + return address + @command('') def addressconvert(self, address): """Convert to/from Legacy <-> Cash Address. Address can be either @@ -176,6 +186,28 @@ def addressconvert(self, address): 'legacy' : addr.to_full_string(Address.FMT_LEGACY), } + @command('') + def addressconvert_slp(self, address): + """Convert to/from Legacy <-> Cash Address or slp Address. Address can be either + a legacy or a Cash Address and both forms will be returned as a JSON + dict.""" + try: + addr = Address.from_string(address) + except Exception as e: + raise AddressError(f'Invalid address: {address}') from e + + if self.config.get("allow_cli_slp_address_conversion") != True: + print("WARNING: If you are converting from legacy or cash address to slp format you need \n" + + "to make sure the receiving wallet is compatible with slp tokens protocol. If the wallet \n" + + "is not compatible with slp, the receiver's wallet will easily burn tokens. To enable slp \n" + + "address conversion you must set the config key 'allow_cli_slp_address_conversion' to 'true'.") + else: + return { + 'cashaddr' : addr.to_full_string(Address.FMT_CASHADDR), + 'legacy' : addr.to_full_string(Address.FMT_LEGACY), + 'slpaddr' : addr.to_full_string(Address.FMT_SLPADDR), + } + @command('') def commands(self): """List of commands""" @@ -376,31 +408,23 @@ def createmultisig(self, num, pubkeys): address = bitcoin.hash160_to_p2sh(hash_160(bfh(redeem_script))) return {'address':address, 'redeemScript':redeem_script} - def address_from_string_check_slp(address, wallet): - address = Address.from_string(address) - assert not isinstance(address, str) - slp_addr_str = address.to_full_string(Address.FMT_SLPADDR) - if address in slp_addr_str and not wallet.is_slp: - raise BaseException('Cannot check SLP addresses with a non-SLP type wallet.') - return address - @command('w') def freeze(self, address): """Freeze address. Freeze the funds at one of your wallet\'s addresses""" - address = address_from_string_check_slp(address, self.wallet) + address = self.address_from_string_check_slp(address, self.wallet) return self.wallet.set_frozen_state([address], True) @command('w') def unfreeze(self, address): """Unfreeze address. Unfreeze the funds at one of your wallet\'s address""" - address = address_from_string_check_slp(address, self.wallet) + address = self.address_from_string_check_slp(address, self.wallet) return self.wallet.set_frozen_state([address], False) @command('wp') def getprivatekeys(self, address, password=None): """Get private keys of addresses. You may pass a single wallet address, or a list of wallet addresses.""" def get_pk(address): - address = address_from_string_check_slp(address, self.wallet) + address = self.address_from_string_check_slp(address, self.wallet) return self.wallet.export_private_key(address, password) if isinstance(address, str): @@ -411,7 +435,7 @@ def get_pk(address): @command('w') def ismine(self, address): """Check if address is in wallet. Return true if and only address is in wallet""" - address = address_from_string_check_slp(address, self.wallet) + address = self.address_from_string_check_slp(address, self.wallet) return self.wallet.is_mine(address) @command('') @@ -427,7 +451,7 @@ def validateaddress(self, address): @command('w') def getpubkeys(self, address): """Return the public keys for a wallet address. """ - address = address_from_string_check_slp(address, self.wallet) + address = self.address_from_string_check_slp(address, self.wallet) return self.wallet.get_public_keys(address) @command('w') @@ -549,7 +573,7 @@ def sweep(self, privkey, destination, fee=None, nocheck=False, imax=100): def signmessage(self, address, message, password=None): """Sign a message with a key. Use quotes if your message contains whitespaces""" - address = address_from_string_check_slp(address, self.wallet) + address = self.address_from_string_check_slp(address, self.wallet) sig = self.wallet.sign_message(address, message, password) return base64.b64encode(sig).decode('ascii') @@ -980,8 +1004,16 @@ def getunusedaddress(self): """Returns the first unused address of the wallet, or None if all addresses are used. An address is considered as used if it has received a transaction, or if it is used in a payment request.""" fmt = Address.FMT_CASHADDR - if self.wallet.is_slp: - fmt = Address.FMT_SLPADDR + addr = self.wallet.get_unused_address() + if addr: + return addr.to_full_string(fmt) + return None + + @command('w') + def getunusedaddress_slp(self): + """Returns the first unused address of the wallet using slp format, or None if all addresses are used. + An address is considered as used if it has received a transaction, or if it is used in a payment request.""" + fmt = Address.FMT_SLPADDR addr = self.wallet.get_unused_address() if addr: return addr.to_full_string(fmt) @@ -1019,13 +1051,13 @@ def signrequest(self, address, password=None): alias_addr = (data and data.get('address')) or None if not alias_addr: raise RuntimeError('Alias could not be resolved') - address_from_string_check_slp(address, self.wallet) # throws with slp address format in non-slp wallets + self.address_from_string_check_slp(address, self.wallet) # throws with slp address format in non-slp wallets self.wallet.sign_payment_request(address, alias, alias_addr, password) @command('w') def rmrequest(self, address): """Remove a payment request""" - address_from_string_check_slp(address, self.wallet) # throws with slp address format in non-slp wallets + self.address_from_string_check_slp(address, self.wallet) # throws with slp address format in non-slp wallets return self.wallet.remove_payment_request(address, self.config) @command('w') diff --git a/electroncash/networks.py b/electroncash/networks.py index 63e4e3474720..f0c477c04187 100644 --- a/electroncash/networks.py +++ b/electroncash/networks.py @@ -43,6 +43,7 @@ class AbstractNet: LEGACY_POW_RETARGET_BLOCKS = LEGACY_POW_TARGET_TIMESPAN // LEGACY_POW_TARGET_INTERVAL # 2016 blocks BASE_UNITS = {'BCH': 8, 'mBCH': 5, 'bits': 2} DEFAULT_UNIT = "BCH" + SLP_PREFLIGHT_CHECK = True # pings other node implementations in the slp network before signing class MainNet(AbstractNet): @@ -161,6 +162,8 @@ class TestNet4(TestNet): asert_daa = ASERTDaa(is_testnet=True) # Redeclare to get instance for this subclass asert_daa.anchor = Anchor(height=16844, bits=486604799, prev_time=1605451779) + # disable slp pre-flight since this is a testnet with no known slp network servers + SLP_PREFLIGHT_CHECK = False class ScaleNet(TestNet): @@ -188,6 +191,8 @@ class ScaleNet(TestNet): asert_daa = ASERTDaa(is_testnet=False) # Despite being a "testnet", ScaleNet uses 2d half-life asert_daa.anchor = None # Intentionally not specified because it's after checkpoint; blockchain.py will calculate + # disable slp pre-flight since this is a testnet with no known slp network servers + SLP_PREFLIGHT_CHECK = False # All new code should access this to get the current network config. net = MainNet diff --git a/electroncash/slp_checker.py b/electroncash/slp_checker.py index 02784cc6bde5..fe99276bf487 100644 --- a/electroncash/slp_checker.py +++ b/electroncash/slp_checker.py @@ -1,6 +1,6 @@ import json from .util import NotEnoughFundsSlp, NotEnoughUnfrozenFundsSlp, print_error -from . import slp +from . import slp, networks from .slp import SlpParsingError, SlpInvalidOutputMessage, SlpUnsupportedSlpTokenType from .slp_preflight_check import SlpPreflightCheck from .transaction import Transaction @@ -250,10 +250,11 @@ def check_tx_slp(wallet, tx, *, coins_to_burn=None, amt_to_burn=None, require_tx raise BadSlpOutpointType('Transaction token receiver vout is not P2PKH' \ + ' or P2SH output type') - # perform slp pre-flight check before signing (this check run here and also at signing) - slp_preflight = SlpPreflightCheck.query(tx, selected_slp_coins=coins_to_burn, amt_to_burn=amt_to_burn) - if not slp_preflight['ok']: - raise Exception("slp pre-flight check failed: %s\n\n(node: %s)"%(slp_preflight.get('invalid_reason', json.dumps(slp_preflight)), slp_preflight['node'])) + # perform slp pre-flight check + if networks.net.SLP_PREFLIGHT_CHECK: + slp_preflight = SlpPreflightCheck.query(tx, selected_slp_coins=coins_to_burn, amt_to_burn=amt_to_burn) + if not slp_preflight['ok']: + raise Exception("slp pre-flight check failed: %s\n\n(node: %s)"%(slp_preflight.get('invalid_reason', json.dumps(slp_preflight)), slp_preflight['node'])) # return True if this check passes print_error("Final SLP check passed") diff --git a/electroncash/slp_dagging.py b/electroncash/slp_dagging.py index ffce4d25f6aa..b9630d36fba6 100644 --- a/electroncash/slp_dagging.py +++ b/electroncash/slp_dagging.py @@ -250,8 +250,7 @@ def has_txid(self, txid): def run(self,): """ Wrapper for mainloop() to manage run state. """ - # Check if we need to reset the graph due to having - # nodes in an undetermined state. + # Check if we need to reset the graph self.graph.maybe_reset() with self._statelock: @@ -394,6 +393,7 @@ def skip_callback(txid): if self.debug > 0: print("DEBUG-DAG: SKIPPING: " + txid) node = self.graph.get_node(txid) + node.graph.gs_skipped = True node.set_validity(False, 2) # temp for debugging @@ -409,10 +409,17 @@ def dl_callback(tx): # f.write(txid+","+str(self.currentdepth)+",true,\n") node = self.graph.get_node(txid) + + # try to get validity from wallet cache try: val = self.validitycache[txid] except KeyError: val = None + + # check the graph's internal validity cache + if val == None and txid in self.graph._valid_txids: + val = 1 + try: node.load_tx(tx, cached_validity=val) except DoubleLoadException: @@ -627,7 +634,7 @@ def add_job(self, job): if job in self.all_jobs: raise ValueError self.all_jobs.add(job) - self.jobs_pending.put((job.height, next(unique), job)) + self.jobs_pending.put((job.height, next(unique), job)) self.wakeup.set() def _stop_all_common(self, job): @@ -811,7 +818,7 @@ class TokenGraph: """ debugging = False - def __init__(self, validator): + def __init__(self, validator, valid_txids=None): self.validator = validator self._nodes = dict() # txid -> Node @@ -827,13 +834,19 @@ def __init__(self, validator): # create singletons for pruning self.prunednodes = {v:NodeInactive(v, None) for v in validator.validity_states.keys()} + # internal validity cache when the graph needs reset + self.gs_skipped = False + self._valid_txids = set() + if valid_txids != None: + self._valid_txids = valid_txids + # Threading rule: we never call node functions while locked. # self._lock = ... # threading not enabled. def reset(self, ): # copy nodes and reset self prevnodes = self._nodes - TokenGraph.__init__(self, self.validator) + TokenGraph.__init__(self, self.validator, self._valid_txids) # nuke Connections to encourage prompt GC for n in prevnodes.values(): @@ -844,14 +857,11 @@ def reset(self, ): pass # This is used by a ValidationJob to determine if the graph - # should be reset. At the beginning of a validation job - # having active nodes with validity==0 causes the job to fail. + # should be reset. If graph search skipped any nodes, then + # the graph needs to be reset. def maybe_reset(self): - nodes = self._nodes.copy() - for node in nodes.values(): - if node.validity == 0 and isinstance(node, Node): - self.reset() - return + if self.gs_skipped: + self.reset() def debug(self, formatstr, *args): if self.debugging: @@ -966,6 +976,11 @@ def get_valid_txids(self, max_size=-1, exclude=None): txid not in exclude: candidate_txids.append(txid) + # join the different validity caches + candidate_txids = set(candidate_txids) + candidate_txids |= self._valid_txids + candidate_txids = list(candidate_txids) + # depending on the number of txids available # either return all txids, or return random txids if len(candidate_txids) <= max_size: @@ -1152,6 +1167,10 @@ def _inactivate_self(self, keepinfo, validity): # Replace self with NodeInactive instance according to keepinfo and validity # no thread locking here, this only gets called internally. + # if valid then we save as valid internally for graph search resets + if validity == 1: + self.graph._valid_txids.add(self.txid) + if keepinfo: replacement = NodeInactive(validity, self.outputs) else: diff --git a/electroncash/slp_graph_search.py b/electroncash/slp_graph_search.py index 69f0334e4268..9efa0de4a539 100644 --- a/electroncash/slp_graph_search.py +++ b/electroncash/slp_graph_search.py @@ -121,33 +121,27 @@ def get_job_cache(self, *, max_size=-1, is_mint=False): token_id = self.valjob.graph.validator.token_id_hex gs_cache = [] + # get valid txid cache from the graph + gs_cache = self.valjob.graph.get_valid_txids(max_size=max_size, exclude=gs_cache) + # pull valid txids from wallet storage + wallet_cache = [] wallet_val = self.valjob.validitycache.copy() wallet_tok_info = wallet.tx_tokinfo.copy() for txid, val in wallet_val.items(): _token_id = wallet_tok_info.get(txid, {}).get("token_id", None) if _token_id == token_id and val == 1: - gs_cache.append(txid) - - # pull valid txids from the shared in-memory token graph - # and prioritize items from the wallet validity cache - if not is_mint: - sample_size = -1 - if max_size > 0 and len(gs_cache) < max_size: - sample_size = max_size - len(gs_cache) - if sample_size > 0: - for txid in self.valjob.graph.get_valid_txids(max_size=sample_size, exclude=gs_cache): - gs_cache.append(txid) - - # TODO: pull valid txids from a "checkpoints" file shipped with the wallet - # these txids can be selected intelligently through graph analysis. Tokens - # supported in the type of arrangement would likely be done through the - # support of the token issuer for the purpose of improving user experience. + wallet_cache.append(txid) # if required limit the size of the cache gs_cache = list(set(gs_cache)) - if gs_cache and max_size > 0 and len(gs_cache) > max_size: - gs_cache = list(set(random.choices(gs_cache, k=max_size))) + if gs_cache and max_size > 0 and len(gs_cache) + len(wallet_cache) > max_size: + gs_cache = random.choices(gs_cache, k=max_size) + + # guarantee inclusion of txids in cache + for txid in wallet_cache: + gs_cache.append(txid) + gs_cache = list(set(gs_cache)) # update the cache size variable used in the UI self.validity_cache_size = len(gs_cache) @@ -396,7 +390,7 @@ def search_query(self, job): elif kind == 'bchd': root_hash_b64 = base64.standard_b64encode(codecs.decode(job.root_txid,'hex')[::-1]).decode("ascii") url = host + "/v1/GetSlpGraphSearch" - cache = job.get_job_cache(max_size=100) + cache = job.get_job_cache(max_size=1000) # bchd needs the reverse txid and then use base64 encoding tx_hashes = [] diff --git a/electroncash/slp_validator_0x01_nft1.py b/electroncash/slp_validator_0x01_nft1.py index f8a4c24c84bb..fbb247f25740 100644 --- a/electroncash/slp_validator_0x01_nft1.py +++ b/electroncash/slp_validator_0x01_nft1.py @@ -17,6 +17,7 @@ from .bitcoin import TYPE_SCRIPT from .util import print_error from .slp_validator_0x01 import Validator_SLP1, GraphContext +from .waitgroup import WaitGroup # from . import slp_proxying # first time loading this module starts a thread. from .slp_graph_search import slp_gs_mgr # first time loading this module starts a thread. @@ -115,7 +116,7 @@ def proxy_cb(txids, results): def fetch_hook(txids, val_job): l = [] - if nft_type == 'SLP129': + if 'SLP129' in nft_type: nonlocal first_fetch_complete gs_job = slp_gs_mgr.get_gs_job(val_job) @@ -181,7 +182,7 @@ def done_callback(job): was_reset=reset, ref=wallet, **kwargs) - elif nft_type == 'SLP129': + elif 'SLP129' in nft_type: job = ValidationJob(graph, txid, network, fetch_hook=fetch_hook, validitycache=wallet.slpv1_validity, @@ -195,7 +196,8 @@ def done_callback(job): raise RuntimeError('Invalid NFT type provided.') job.add_callback(done_callback) - job_mgr.add_job(job) + if nft_type != 'SLP129_65': + job_mgr.add_job(job) return job class ValidationJobNFT1Child(ValidationJob): @@ -236,6 +238,8 @@ def __init__(self, token_id_hex, jobmgr): self.token_id_hex = token_id_hex self.validation_jobmgr = jobmgr + self.wg = WaitGroup() # allows the validator to pause the thread as needed (e.g. for waiting on network thread) + def get_info(self, tx, *, diff_testing_mode=False): """ Enforce internal consensus rules (check all rules that don't involve @@ -332,17 +336,17 @@ def check_needed(self, myinfo, out_n): else: return (out_n > 0) - def download_nft_genesis(self, nft_child_job, done_callback): + def download_nft_genesis(self, nft_child_job): wallet = nft_child_job.ref() + wg = self.wg def dl_cb(resp): if wallet == None: return if resp.get('error', None): - #raise Exception(resp['error'].get('message')) - if done_callback: - done_callback(False) + raise Exception(resp['error'].get('message')) + #start_dl_nft_parent(nft_child_job, False) else: raw = resp.get('result') tx = Transaction(raw) @@ -362,25 +366,29 @@ def dl_cb(resp): wallet.tx_tokinfo[txid] = tti wallet.save_transactions() nft_child_job.genesis_tx = tx - if done_callback: - done_callback(True) + wg.clear() if wallet.transactions.get(self.token_id_hex, None): dl_cb({'result': wallet.transactions[self.token_id_hex].serialize()}) else: + self.wg.add(1) requests = [('blockchain.transaction.get', [self.token_id_hex]), ] nft_child_job.network.send(requests, dl_cb) + self.wg.wait() + + self.start_dl_nft_parent(nft_child_job, True) - def download_nft_parent_tx(self, nft_child_job, done_callback): + def download_nft_parent_tx(self, nft_child_job): wallet = nft_child_job.ref() + wg = self.wg def dl_cb(resp): if wallet == None: return if resp.get('error'): - if done_callback: - done_callback(False) + raise Exception("error") + #start_nft_parent_validation(nft_child_job, False) else: raw = resp.get('result') tx = Transaction(raw) @@ -412,17 +420,20 @@ def dl_cb(resp): wallet.tx_tokinfo[txid] = tti wallet.save_transactions() nft_child_job.nft_parent_tx = tx - if done_callback: - done_callback(True) + wg.clear() nft_parent_txid = nft_child_job.genesis_tx.inputs()[0]['prevout_hash'] if wallet.transactions.get(nft_parent_txid, None): dl_cb({'result': wallet.transactions[nft_parent_txid].serialize()}) else: + self.wg.add(1) requests = [('blockchain.transaction.get', [nft_parent_txid]), ] nft_child_job.network.send(requests, dl_cb) + self.wg.wait() + + self.start_nft_parent_validation(nft_child_job, True) - def start_NFT_parent_job(self, nft_child_job, done_callback): + def start_NFT_parent_job(self, nft_child_job): wallet = nft_child_job.ref() network = nft_child_job.network @@ -431,10 +442,7 @@ def start_NFT_parent_job(self, nft_child_job, done_callback): slp_gs_mgr.slp_validity_signal.emit(nft_child_job.nft_parent_tx.txid_fast(), nft_child_job.nft_parent_validity) slp_gs_mgr.slp_validity_signal.emit(nft_child_job.genesis_tx.txid_fast(), 4) slp_gs_mgr.slp_validity_signal.emit(nft_child_job.root_txid, 4) - if done_callback: - done_callback(nft_child_job.nft_parent_validity) - else: - raise Exception("no done_callback") + self.restart_nft_job(nft_child_job, nft_child_job.nft_parent_validity) return def callback(job): @@ -461,66 +469,46 @@ def callback(job): if slp_gs_mgr.slp_validity_signal: slp_gs_mgr.slp_validity_signal.emit(txid, val) #slp_gs_mgr.slp_validity_signal.emit(nft_child_job.genesis_tx.txid_fast(), val) - - if done_callback: - done_callback(val) - else: - raise Exception("no done_callback") + nft_child_job.graph.validator.restart_nft_job(nft_child_job, val) tx = nft_child_job.nft_parent_tx job = self.validation_jobmgr.graph_context and \ - self.validation_jobmgr.graph_context.make_job(tx, wallet, network, nft_type='SLP129', + self.validation_jobmgr.graph_context.make_job(tx, wallet, network, nft_type='SLP129_65', debug=nft_child_job.debug, reset=nft_child_job.was_reset) if job is not None: job.add_callback(callback) + job.run() elif self.validation_jobmgr.graph_context is None: # FIXME? #raise Exception("Graph Context is None, JobManager was killed") warnings.warn("Graph Context is None, JobManager was killed") else: raise Exception("NO JOB!") - with wallet.lock: - wallet.tx_tokinfo[nft_child_job.genesis_tx.txid_fast()]['validity'] = 4 - wallet.save_transactions() - if slp_gs_mgr.slp_validity_signal: - slp_gs_mgr.slp_validity_signal.emit(nft_child_job.genesis_tx.txid_fast(), 4) - if done_callback: - done_callback(4) - def validate_NFT_parent(self, nft_child_job, myinfo): - def restart_nft_job(val): - nft_child_job.nft_parent_validity = val - try: - self.validation_jobmgr.unpause_job(nft_child_job) - except: - if nft_child_job.running: - nft_child_job.paused = False - else: nft_child_job.run() - - def start_nft_parent_validation(success): - if success: - self.start_NFT_parent_job(nft_child_job, done_callback=restart_nft_job) - else: - restart_nft_job(4) + def restart_nft_job(self, nft_child_job, val): + nft_child_job.nft_parent_validity = val - def start_dl_nft_parent(success): - if success: - self.download_nft_parent_tx(nft_child_job, done_callback=start_nft_parent_validation) - else: - restart_nft_job(2) + def start_nft_parent_validation(self, nft_child_job, success): + if success: + self.start_NFT_parent_job(nft_child_job) + else: + self.restart_nft_job(nft_child_job, 4) - paused = self.validation_jobmgr.pause_job(nft_child_job) - if not paused: - raise Exception("NFT1 child job was not paused") - self.download_nft_genesis(nft_child_job, start_dl_nft_parent) + def start_dl_nft_parent(self, nft_child_job, success): + if success: + self.download_nft_parent_tx(nft_child_job) + else: + self.restart_nft_job(nft_child_job, 2) + + def validate_NFT_parent(self, nft_child_job, myinfo): + self.download_nft_genesis(nft_child_job) def validate(self, myinfo, inputs_info): - current_job = self.validation_jobmgr.job_current - if isinstance(current_job, ValidationJobNFT1Child): - nft_child_job = current_job + if isinstance(self.validation_jobmgr.job_current, ValidationJobNFT1Child): + nft_child_job = self.validation_jobmgr.job_current else: raise Exception("This should never happen. myinfo: " + str(myinfo) + ", inputs_info: " + str(inputs_info)) - + if nft_child_job.nft_parent_validity > 1: return (False, nft_child_job.nft_parent_validity) @@ -540,7 +528,9 @@ def validate(self, myinfo, inputs_info): parent_slp_msg = SlpMessage.parseSlpOutputScript(parent_tx.outputs()[0][1]) except SlpInvalidOutputMessage: return (False, 4) - if parent_slp_msg.transaction_type in ['GENESIS', 'MINT'] and parent_slp_msg.op_return_fields['initial_token_mint_quantity'] < 1: + if parent_slp_msg.transaction_type == 'GENESIS' and parent_slp_msg.op_return_fields['initial_token_mint_quantity'] < 1: + return (False, 3) + elif parent_slp_msg.transaction_type == 'MINT' and parent_slp_msg.op_return_fields['additional_token_quantity'] < 1: return (False, 3) elif parent_slp_msg.transaction_type == 'SEND' and sum(parent_slp_msg.op_return_fields['token_output']) < 1: return (False, 3) diff --git a/electroncash/waitgroup.py b/electroncash/waitgroup.py new file mode 100644 index 000000000000..c1bd0218ee82 --- /dev/null +++ b/electroncash/waitgroup.py @@ -0,0 +1,36 @@ +# source: https://gist.github.com/pteichman/84b92ae7cef0ab98f5a8 + +import threading # :( + +class WaitGroup(object): + """WaitGroup is like Go sync.WaitGroup. + + Without all the useful corner cases. + """ + def __init__(self): + self.count = 0 + self.cv = threading.Condition() + + def add(self, n): + self.cv.acquire() + self.count += n + self.cv.release() + + def done(self): + self.cv.acquire() + self.count -= 1 + if self.count == 0: + self.cv.notify_all() + self.cv.release() + + def clear(self): + self.cv.acquire() + self.count = 0 + self.cv.notify_all() + self.cv.release() + + def wait(self): + self.cv.acquire() + while self.count > 0: + self.cv.wait() + self.cv.release() diff --git a/electroncash_gui/qt/main_window.py b/electroncash_gui/qt/main_window.py index 39cd222802bb..2c3d5ced5ee6 100644 --- a/electroncash_gui/qt/main_window.py +++ b/electroncash_gui/qt/main_window.py @@ -79,7 +79,6 @@ import electroncash.slp as slp from electroncash.slp_coinchooser import SlpCoinChooser from electroncash.slp_checker import SlpTransactionChecker -from electroncash.slp_preflight_check import SlpPreflightCheck from .amountedit import SLPAmountEdit from electroncash.util import format_satoshis_nofloat from .slp_create_token_genesis_dialog import SlpCreateTokenGenesisDialog diff --git a/electroncash_gui/qt/network_dialog.py b/electroncash_gui/qt/network_dialog.py index ba2e55a47958..535e1b6930be 100644 --- a/electroncash_gui/qt/network_dialog.py +++ b/electroncash_gui/qt/network_dialog.py @@ -416,6 +416,7 @@ def create_menu(self, position): menu = QMenu() menu.addAction(_("Copy Txid"), lambda: self._copy_txid_to_clipboard()) menu.addAction(_("Copy Reversed Txid"), lambda: self._copy_txid_to_clipboard(True)) + menu.addAction(_("Copy Status"), lambda: self._copy_status_to_clipboard()) menu.addAction(_("Refresh List"), lambda: self.update()) txid = item.data(0, Qt.UserRole) if item.data(4, Qt.UserRole) in ['Exited']: @@ -430,6 +431,10 @@ def _copy_txid_to_clipboard(self, flip_bytes=False): txid = codecs.encode(codecs.decode(txid,'hex')[::-1], 'hex').decode() qApp.clipboard().setText(txid) + def _copy_status_to_clipboard(self, flip_bytes=False): + status = self.currentItem().data(4, Qt.UserRole) + qApp.clipboard().setText(status) + def restart_job(self, txid): job = slp_gs_mgr.find(txid) if job: @@ -504,7 +509,7 @@ def update(self): x = QTreeWidgetItem([job.root_txid[:6], tx_count, self.humanbytes(job.gs_response_size), str(job.validity_cache_size), status + exit_msg]) x.setData(0, Qt.UserRole, k) x.setData(3, Qt.UserRole, job.validity_cache_size) - x.setData(4, Qt.UserRole, status) + x.setData(4, Qt.UserRole, status + exit_msg) if status == 'Downloading...': working_item = x elif status == "Downloaded":