Example #1
0
    def handle_genesis_tx(self, tx):
        txid = tx.txid()

        try:
            slpMsg = SlpMessage.parseSlpOutputScript(tx.outputs()[0][1])
        except SlpUnsupportedSlpTokenType as e:
            return self.fail_genesis_info(
                _("Unsupported SLP token version/type - %r.") % (e.args[0], ))
        except SlpInvalidOutputMessage as e:
            return self.fail_genesis_info(
                _("This transaction does not contain a valid SLP message.\nReason: %r."
                  ) % (e.args, ))
        if slpMsg.transaction_type != 'GENESIS':
            return self.fail_genesis_info(
                _("This is an SLP transaction, however it is not a genesis transaction."
                  ))

        slpMsg.op_return_fields['token_id'] = txid
        self.tokens.append(slpMsg.op_return_fields)

        dir = os.path.join(tempfile.gettempdir(), 'waifu')
        try:
            os.makedirs(dir)
        except:
            pass
        file = os.path.join(dir, f"{txid}.png")
        url = f"https://icons.waifufaucet.com/original/{txid}.png"
        if not os.path.isfile(file):
            urllib.request.urlretrieve(url, file)
        name = slpMsg.op_return_fields['token_name'].decode("utf-8")
        item = QListWidgetItem(QIcon(file), name)
        self.listWidget.addItem(item)
 def build_slp_txn(coins, slp_output, receiver_output, postoffice_output,
                   change_output):
     slp_msg = SlpMessage.parseSlpOutputScript(slp_output[1])
     outputs = [slp_output, receiver_output]
     if len(slp_msg.op_return_fields["token_output"]) - 1 == 2:
         outputs.extend([postoffice_output])
     elif len(slp_msg.op_return_fields["token_output"]) - 1 == 3:
         outputs.extend([postoffice_output, change_output])
     tx = Transaction.from_io(coins, outputs)
     return tx
    def build_slp_txn(coins, slp_output, pre_postage_outputs,
                      postoffice_output, change_output, send_amount,
                      old_slp_msg):
        slp_msg = SlpMessage.parseSlpOutputScript(slp_output[1])
        pre_postage_outputs = list(pre_postage_outputs[1:])
        if sum(old_slp_msg.op_return_fields["token_output"]
               ) > send_amount:  # has change output
            pre_postage_outputs = pre_postage_outputs[:-1]

        outputs = [slp_output] + pre_postage_outputs
        if len(slp_msg.op_return_fields["token_output"]) - len(
                pre_postage_outputs) == 2:
            outputs.extend([postoffice_output])
        elif len(slp_msg.op_return_fields["token_output"]) - len(
                pre_postage_outputs) == 3:
            outputs.extend([postoffice_output, change_output])
        tx = Transaction.from_io(coins, outputs)
        return tx
    def handle_genesis_tx(self, tx):
        self.token_id_e.setReadOnly(True)
        self.get_info_button.setDisabled(True)
        self.load_tx_menu_button.setDisabled(True)

        self.newtoken_genesis_tx      = tx
        self.view_tx_button.setDisabled(False)

        txid = tx.txid()
        token_id = self.token_id_e.text().strip()
        if token_id and txid != token_id:
            return self.fail_genesis_info(_('TXID does not match token ID!'))
        self.newtoken_token_id = txid
        self.token_id_e.setText(self.newtoken_token_id)

        try:
            slpMsg = SlpMessage.parseSlpOutputScript(tx.outputs()[0][1])
        except SlpUnsupportedSlpTokenType as e:
            return self.fail_genesis_info(_("Unsupported SLP token version/type - %r.")%(e.args[0],))
        except SlpInvalidOutputMessage as e:
            return self.fail_genesis_info(_("This transaction does not contain a valid SLP message.\nReason: %r.")%(e.args,))
        if slpMsg.transaction_type != 'GENESIS':
            return self.fail_genesis_info(_("This is an SLP transaction, however it is not a genesis transaction."))


        f_fieldnames = QTextCharFormat()
        f_fieldnames.setFont(QFont(MONOSPACE_FONT))
        f_normal = QTextCharFormat()

        self.token_info_e.clear()
        cursor = self.token_info_e.textCursor()

        fields = [
            ('ticker', _('ticker'), 'utf8', None),
            ('token_name', _('name'), 'utf8', None),
            ('token_doc_url', _('doc url'), 'ascii', 'html'),
            ('token_doc_hash', _('doc hash'), 'hex', None),
                 ]

        cursor.insertText(_('Issuer-declared strings in genesis:'))
        cursor.insertBlock()
        for k,n,e,f in fields:
            data = slpMsg.op_return_fields[k]
            if e == 'hex':
                friendlystring = None
            else:
                # Attempt to make a friendly string, or fail to hex
                try:
                    # Ascii only
                    friendlystring = data.decode(e) # raises UnicodeDecodeError with bytes > 127.

                    # Count ugly characters (that need escaping in python strings' repr())
                    uglies = 0
                    for b in data:
                        if b < 0x20 or b == 0x7f:
                            uglies += 1
                    # Less than half of characters may be ugly.
                    if 2*uglies >= len(data):
                        friendlystring = None
                except UnicodeDecodeError:
                    friendlystring = None

            if len(data) == 0:
                showstr = '(empty)'
                f=None
            elif friendlystring is None:
                showstr = data.hex()
                f=None
            else:
                showstr = repr(friendlystring)

            cursor.insertText(' '*(10 - len(n)) + n + ': ', f_fieldnames)
            if f == 'html':
                enc_url  = html.escape(friendlystring)
                enc_text = html.escape(showstr)
                cursor.insertHtml('<a href="%s" title="%s">%s</a>'%(enc_url, enc_url, enc_text))
            else:
                cursor.insertText(showstr, f_normal)
            cursor.insertBlock()

        self.newtoken_decimals = slpMsg.op_return_fields['decimals']
        cursor.insertText(_('Decimals:') + ' ' + str(self.newtoken_decimals))
        cursor.insertBlock()

        numtokens = format_satoshis_nofloat(slpMsg.op_return_fields['initial_token_mint_quantity'],
                                    num_zeros=self.newtoken_decimals,
                                    decimal_point=self.newtoken_decimals,)
        mbv = slpMsg.op_return_fields['mint_baton_vout']
        if mbv is None or mbv > len(tx.outputs()):
            issuance_type = _('Initial issuance type: fixed supply')
        else:
            issuance_type = _('Initial issuance type: flexible supply')

        cursor.insertText(_('Initial issuance:') + ' ' + numtokens)
        cursor.insertBlock()
        cursor.insertText(issuance_type)

        #cursor.insertBlock()

        self.newtoken_genesis_message = slpMsg

        self.add_button.setDisabled(False)
    def burn_token(self, preview=False, multisig_tx_to_sign=None):
        token_type = self.wallet.token_types[self.token_id_e.text()]['class']
        unfrozen_token_qty = self.wallet.get_slp_token_balance(
            self.token_id_e.text(), self.main_window.config)[3]
        desired_burn_amt = self.token_qty_e.get_amount()

        selected_slp_coins = []

        if desired_burn_amt == None or desired_burn_amt < 0:
            self.show_message(_("Enter a valid token quantity."))
            return
        elif desired_burn_amt > unfrozen_token_qty:
            self.show_message(
                _("Cannot burn a quantity greater than your unfrozen token balance."
                  ))
            return

        msg = "Destroy " + self.token_qty_e.text(
        ) + " " + self.token_name.text() + " tokens"
        if self.token_burn_baton_cb.isChecked():
            msg += " AND the MINTING BATON"
        msg += "?!?"
        reply = QMessageBox.question(self, "Continue?", msg, QMessageBox.Yes,
                                     QMessageBox.No)
        if reply == QMessageBox.Yes:
            pass
        else:
            return

        outputs = []

        addr = self.wallet.get_unused_address(frozen_ok=False)
        if addr is None:
            if not self.wallet.is_deterministic():
                addr = self.wallet.get_receiving_address()
            else:
                addr = self.wallet.create_new_address(True)

        try:
            slp_coins = self.wallet.get_slp_utxos(
                self.token_id_e.text(),
                domain=None,
                exclude_frozen=True,
                confirmed_only=self.main_window.config.get(
                    'confirmed_only', False),
                slp_include_invalid=
                False,  #self.token_burn_invalid_cb.isChecked(),
                slp_include_baton=self.token_burn_baton_cb.isChecked())
            if multisig_tx_to_sign is None:
                selected_slp_coins = []
                if desired_burn_amt < unfrozen_token_qty:
                    total_amt_added = 0
                    for coin in slp_coins:
                        if coin['token_value'] != "MINT_BATON" and coin[
                                'token_validation_state'] == 1:
                            if coin['token_value'] >= desired_burn_amt:
                                selected_slp_coins.append(coin)
                                total_amt_added += coin['token_value']
                                break
                    if total_amt_added < desired_burn_amt:
                        for coin in slp_coins:
                            if coin['token_value'] != "MINT_BATON" and coin[
                                    'token_validation_state'] == 1:
                                if total_amt_added < desired_burn_amt:
                                    selected_slp_coins.append(coin)
                                    total_amt_added += coin['token_value']
                    if total_amt_added > desired_burn_amt:
                        slp_op_return_msg = buildSendOpReturnOutput_V1(
                            self.token_id_e.text(),
                            [total_amt_added - desired_burn_amt], token_type)
                        outputs.append(slp_op_return_msg)
                        outputs.append((TYPE_ADDRESS, addr, 546))
                else:
                    for coin in slp_coins:
                        if coin['token_value'] != "MINT_BATON" and coin[
                                'token_validation_state'] == 1:
                            selected_slp_coins.append(coin)
            else:
                try:
                    slp_msg = SlpMessage.parseSlpOutputScript(
                        multisig_tx_to_sign.outputs()[0][1])
                except SlpParsingError:
                    slp_msg = None
                if slp_msg and slp_msg.op_return_fields[
                        'token_id_hex'] != self.token_id_e.text():
                    self.show_message(_("Token id in the imported transaction is not correct.")+\
                                            _("\n\nImported Token ID: ") + slp_msg.op_return_fields['token_id_hex'] + \
                                            _("\n\nDesired Token ID: ") + self.token_id_e.text())
                    return
                for txo in multisig_tx_to_sign.inputs():
                    addr = txo['address']
                    prev_out = txo['prevout_hash']
                    prev_n = txo['prevout_n']
                    slp_txo = None
                    try:
                        for coin in slp_coins:
                            if coin['prevout_hash'] == prev_out \
                                and coin['prevout_n'] == prev_n \
                                and coin['token_value'] != "MINT_BATON":
                                selected_slp_coins.append(coin)
                                total_burn_amt += coin['token_value']
                    except KeyError:
                        pass
                if slp_msg:
                    total_burn_amt -= sum(
                        slp_msg.op_return_fields['token_output'])
                if total_burn_amt > desired_burn_amt:
                    if slp_msg:
                        self.show_message(
                            _("Amount burned in transaction does not match the amount specified."
                              ))
                    else:
                        self.show_message(_("Amount burned in transaction does not match the amount specified.") + \
                                        _("\n\nMake sure the Token ID displayed in the Burn Tool dialog matches the token that you are trying to burn."))
                    return

        except OPReturnTooLarge:
            self.show_message(
                _("Optional string text causing OP_RETURN greater than 223 bytes."
                  ))
            return
        except Exception as e:
            traceback.print_exc(file=sys.stdout)
            self.show_message(str(e))
            return

        if self.token_burn_baton_cb.isChecked():
            for coin in slp_coins:
                if coin['token_value'] == "MINT_BATON" and coin[
                        'token_validation_state'] == 1:
                    selected_slp_coins.append(coin)

        # if self.token_burn_invalid_cb.isChecked():
        #     for coin in slp_coins:
        #         if coin['token_validation_state'] != 1:
        #             selected_slp_coins.append(coin)

        try:
            if multisig_tx_to_sign is None:
                bch_change = sum(c['value'] for c in selected_slp_coins)
                outputs.append((TYPE_ADDRESS, addr, bch_change))
                coins = self.main_window.get_coins()
                fixed_fee = None
                tx = self.main_window.wallet.make_unsigned_transaction(
                    coins,
                    outputs,
                    self.main_window.config,
                    fixed_fee,
                    None,
                    mandatory_coins=selected_slp_coins)
            else:
                tx = multisig_tx_to_sign

            # perform slp pre-flight check before signing (this check run here and also at signing)
            slp_preflight = SlpPreflightCheck.query(
                tx,
                selected_slp_coins=selected_slp_coins,
                amt_to_burn=desired_burn_amt)
            if not slp_preflight['ok']:
                raise Exception("slp pre-flight failed: %s" %
                                slp_preflight['invalid_reason'])

        except NotEnoughFunds:
            self.show_message(_("Insufficient funds"))
            return
        except ExcessiveFee:
            self.show_message(_("Your fee is too high.  Max is 50 sat/byte."))
            return
        except BaseException as e:
            traceback.print_exc(file=sys.stdout)
            self.show_message(str(e))
            return

        if preview:
            show_transaction(tx,
                             self.main_window,
                             None,
                             False,
                             self,
                             slp_coins_to_burn=selected_slp_coins,
                             slp_amt_to_burn=desired_burn_amt)
            return

        msg = []

        if self.main_window.wallet.has_password():
            msg.append("")
            msg.append(_("Enter your password to proceed"))
            password = self.main_window.password_dialog('\n'.join(msg))
            if not password:
                return
        else:
            password = None

        tx_desc = None

        def sign_done(success):
            if success:
                if not tx.is_complete():
                    show_transaction(tx, self.main_window, None, False, self)
                    self.main_window.do_clear()
                else:
                    self.main_window.broadcast_transaction(tx, tx_desc)

        self.main_window.sign_tx_with_password(
            tx,
            sign_done,
            password,
            slp_coins_to_burn=selected_slp_coins,
            slp_amt_to_burn=desired_burn_amt)

        self.burn_button.setDisabled(True)
        self.close()
    def __init__(self, tx, parent, desc, prompt_if_unsaved, window_to_close_on_broadcast=None, *, slp_coins_to_burn=None, slp_amt_to_burn=None):
        '''Transactions in the wallet will show their description.
        Pass desc to give a description for txs not yet in the wallet.
        '''
        # We want to be a top-level window
        QDialog.__init__(self, parent=None)
        # Take a copy; it might get updated in the main window by
        # e.g. the FX plugin.  If this happens during or after a long
        # sign operation the signatures are lost.
        self.tx = copy.deepcopy(tx)
        self.tx.deserialize()
        self.main_window = parent
        self.wallet = parent.wallet
        self.prompt_if_unsaved = prompt_if_unsaved
        self.window_to_close_on_broadcast = window_to_close_on_broadcast
        self.saved = False
        self.desc = desc
        self.cashaddr_signal_slots = []
        self._dl_pct = None
        self._closed = False
        self.tx_hash = self.tx.txid_fast() if self.tx.raw and self.tx.is_complete() else None
        self.slp_token_id_label = None
        self.tx_height = None
        self.slp_coins_to_burn = slp_coins_to_burn
        self.slp_amt_to_burn = slp_amt_to_burn
        
        # Parse SLP output data
        self.slp_outputs = []
        self.slp_mint_baton_vout = None
        self.slp_info = None
        try:
            slp_msg = SlpMessage.parseSlpOutputScript(tx.outputs()[0][1])
        except:
            pass
        else:
            self.slp_info = slp_msg.op_return_fields
            self.slp_info['type'] = slp_msg.transaction_type
            if self.slp_info['type'] == "GENESIS":
                if self.tx_hash is not None:
                    wallet_dat = self.wallet.token_types[self.tx_hash]
                    self.slp_info['token_id_hex'] = self.tx_hash
                else:
                    wallet_dat = {}
                    wallet_dat['decimals'] = slp_msg.op_return_fields['decimals']
                    wallet_dat['name'] = "Not in wallet"
                    self.slp_info['token_id_hex'] = "Unknown until transaction is complete"
            else:
                try:
                    wallet_dat = self.wallet.token_types[slp_msg.op_return_fields['token_id_hex']]
                except KeyError:
                    wallet_dat = {}
                    wallet_dat['decimals'] = "?"
                    wallet_dat['name'] = "Not in wallet"

            self.slp_info['name'] = wallet_dat['name']
            dec = wallet_dat['decimals']
            if isinstance(dec, int):
                if self.slp_info['type'] == "GENESIS":
                    self.slp_outputs.append(0)
                    mint = self.slp_info['initial_token_mint_quantity']
                    self.slp_outputs.append(format_satoshis_nofloat(mint, decimal_point=dec, num_zeros=dec))
                    self.slp_mint_baton_vout = self.slp_info['mint_baton_vout']
                elif self.slp_info['type'] == "MINT":
                    self.slp_outputs.append(0)
                    mint = self.slp_info['additional_token_quantity']
                    self.slp_outputs.append(format_satoshis_nofloat(mint, decimal_point=dec, num_zeros=dec))
                    self.slp_mint_baton_vout = self.slp_info['mint_baton_vout']
                elif self.slp_info['type'] == "SEND":
                    for i, o in enumerate(self.slp_info['token_output']):
                        self.slp_outputs.append(format_satoshis_nofloat(o, decimal_point=dec, num_zeros=dec))

        Weak.finalization_print_error(self)  # track object lifecycle

        self.setMinimumWidth(750)
        self.setWindowTitle(_("Transaction"))

        vbox = QVBoxLayout()
        self.setLayout(vbox)

        self.tx_hash_e  = ButtonsLineEdit()
        l = QLabel(_("&Transaction ID:"))
        l.setBuddy(self.tx_hash_e)
        vbox.addWidget(l)
        self.tx_hash_e.addCopyButton()
        weakSelfRef = Weak.ref(self)
        qr_show = lambda: weakSelfRef() and weakSelfRef().main_window.show_qrcode(str(weakSelfRef().tx_hash_e.text()), _("Transaction ID"), parent=weakSelfRef())
        icon = ":icons/qrcode_white.svg" if ColorScheme.dark_scheme else ":icons/qrcode.svg"
        self.tx_hash_e.addButton(icon, qr_show, _("Show as QR code"))
        self.tx_hash_e.setReadOnly(True)
        vbox.addWidget(self.tx_hash_e)
        self.tx_desc = QLabel()
        vbox.addWidget(self.tx_desc)
        self.status_label = QLabel()
        vbox.addWidget(self.status_label)
        self.date_label = QLabel()
        vbox.addWidget(self.date_label)
        self.amount_label = QLabel()
        vbox.addWidget(self.amount_label)
        self.size_label = QLabel()
        vbox.addWidget(self.size_label)
        self.fee_label = QLabel()
        vbox.addWidget(self.fee_label)

        for l in (self.tx_desc, self.status_label, self.date_label, self.amount_label, self.size_label, self.fee_label):
            # make these labels selectable by mouse in case user wants to copy-paste things in tx dialog
            l.setTextInteractionFlags(l.textInteractionFlags() | Qt.TextSelectableByMouse)

        def open_be_url(link):
            if link:
                try:
                    kind, thing = link.split(':')
                    url = web.BE_URL(self.main_window.config, kind, thing)
                except:
                    url = None
                if url:
                    webopen( url )
                else:
                    self.show_error(_('Unable to open in block explorer. Please be sure your block explorer is configured correctly in preferences.'))

        self.status_label.linkActivated.connect(open_be_url)

        self.add_io(vbox)

        self.add_slp_info(vbox)

        self.sign_button = b = QPushButton(_("&Sign"))
        b.clicked.connect(self.sign)

        self.broadcast_button = b = QPushButton(_("&Broadcast"))
        b.clicked.connect(self.do_broadcast)
        self.last_broadcast_time = 0

        self.save_button = b = QPushButton(_("S&ave"))
        b.clicked.connect(self.save)

        self.cancel_button = b = CloseButton(self)

        self.qr_button = b = QPushButton()
        b.setIcon(QIcon(icon))
        b.clicked.connect(self.show_qr)
        b.setShortcut(QKeySequence(Qt.ALT + Qt.Key_Q))

        self.copy_button = CopyButton(lambda: str(weakSelfRef() and weakSelfRef().tx),
                                      callback=lambda: weakSelfRef() and weakSelfRef().show_message(_("Transaction raw hex copied to clipboard.")))

        # Action buttons
        self.buttons = [self.sign_button, self.broadcast_button, self.cancel_button]
        # Transaction sharing buttons
        self.sharing_buttons = [self.copy_button, self.qr_button, self.save_button]

        run_hook('transaction_dialog', self)

        hbox = QHBoxLayout()
        hbox.addLayout(Buttons(*self.sharing_buttons))
        hbox.addStretch(1)
        hbox.addLayout(Buttons(*self.buttons))
        vbox.addLayout(hbox)

        self.throttled_update_sig.connect(self.throttled_update, Qt.QueuedConnection)
        self.initiate_fetch_input_data(True)

        self.update()

        # connect slots so we update in realtime as blocks come in, etc
        parent.history_updated_signal.connect(self.update_tx_if_in_wallet)
        parent.labels_updated_signal.connect(self.update_tx_if_in_wallet)
        parent.network_signal.connect(self.got_verified_tx)