def select_coins(wallet,
                     token_id,
                     amount,
                     config,
                     isInvoice=False,
                     *,
                     domain=None):
        token_outputs_amts = []
        if isinstance(amount, list):
            amt = sum(amount) or 0
            token_outputs_amts.extend(amount)
        else:
            amt = amount or 0
            token_outputs_amts.append(amt)

        valid_bal, _, _, unfrozen_bal, _ = wallet.get_slp_token_balance(
            token_id, config)

        if amt > valid_bal:
            raise NotEnoughFundsSlp("Not enough token funds.")
        if valid_bal >= amt > unfrozen_bal:
            raise NotEnoughUnfrozenFundsSlp("Not enough unfrozen token funds.")

        slp_coins = wallet.get_slp_spendable_coins(token_id, domain, config,
                                                   isInvoice)
        slp_coins = sorted(slp_coins, key=lambda k: -k['token_value'])

        selected_slp_coins = []
        total_amt_added = 0
        for coin in slp_coins:
            if total_amt_added < amt:
                selected_slp_coins.append(coin)
                total_amt_added += coin['token_value']
            else:
                break

        slp_op_return_msg = None
        if total_amt_added > 0:
            token_change = total_amt_added - amt
            if token_change > 0:
                token_outputs_amts.append(token_change)
            token_type = wallet.token_types[token_id]['class']
            slp_op_return_msg = slp.buildSendOpReturnOutput_V1(
                token_id, token_outputs_amts, token_type)

        if selected_slp_coins:
            assert slp_op_return_msg

        return (selected_slp_coins, slp_op_return_msg)
    def burn_token(self, preview=False):
        unfrozen_token_qty = self.wallet.get_slp_token_balance(self.token_id_e.text(), self.main_window.config)[3]
        burn_amt = self.token_qty_e.get_amount()
        if burn_amt == None or burn_amt == 0:
            self.show_message(_("Invalid token quantity entered."))
            return
        if burn_amt > unfrozen_token_qty:
            self.show_message(_("Cannot burn more tokens than the unfrozen amount available."))
            return

        reply = QMessageBox.question(self, "Continue?", "Destroy " + self.token_qty_e.text() + " " + self.token_name.text() + " tokens?", QMessageBox.Yes, QMessageBox.No)
        if reply == QMessageBox.Yes:
            pass
        else:
            return

        outputs = []
        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=self.token_burn_invalid_cb.isChecked(), slp_include_baton=self.token_burn_baton_cb.isChecked())

        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:
            selected_slp_coins = []
            if 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'] >= burn_amt:
                            selected_slp_coins.append(coin)
                            total_amt_added+=coin['token_value']
                            break
                if total_amt_added < burn_amt:
                    for coin in slp_coins:
                        if coin['token_value'] != "MINT_BATON" and coin['token_validation_state'] == 1:
                            if total_amt_added < burn_amt:
                                selected_slp_coins.append(coin)
                                total_amt_added+=coin['token_value']
                if total_amt_added > burn_amt:
                    token_type = self.wallet.token_types[self.token_id_e.text()]['class']
                    slp_op_return_msg = buildSendOpReturnOutput_V1(self.token_id_e.text(), [total_amt_added - 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)

        except OPReturnTooLarge:
            self.show_message(_("Optional string text causiing 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)

        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

        try:
            tx = self.main_window.wallet.make_unsigned_transaction(coins, outputs, self.main_window.config, fixed_fee, None, mandatory_coins=selected_slp_coins)
        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)
            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)

        self.burn_button.setDisabled(True)
        self.close()
    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()
Ejemplo n.º 4
0
    def calculate_postage_and_build_slp_msg(wallet, config, tokenId, po_data,
                                            send_amount):

        # determine the amount of postage to pay based on the token's rate and number of inputs we will sign
        weight = po_data["weight"]
        rate = None
        for stamp in po_data["stamps"]:
            if stamp["tokenId"] == tokenId:
                rate = stamp["rate"]

        if rate is None:
            raise Exception(
                "Post Office does not offer postage for tokenId: " + tokenId)

        # variables used for txn size estimation
        slpmsg_output_max_size = 8 + 1 + 73  # case where both postage and change are needed
        slpmsg_output_mid_size = slpmsg_output_max_size - 9  # case where no token change is not needed
        slpmsg_output_min_size = slpmsg_output_mid_size - 9  # case where no token or change are needed
        output_unit_size = 34  # p2pkh output size
        input_unit_size_ecdsa = 149  # approx. size needed for ecdsa signed input
        input_unit_size_schnorr = 141  # approx. size needed for schnorr signed input
        txn_overhead = 4 + 1 + 1 + 4  # txn version, input count varint, output count varint, timelock

        # determine number of stamps required in this while loop
        sats_diff_w_fee = 1  # controls entry into while loop
        stamp_count = -1  # will get updated to 0 stamps in first iteration
        while sats_diff_w_fee > 0:
            stamp_count += 1
            coins, _ = SlpCoinChooser.select_coins(
                wallet, tokenId, (send_amount + (rate * stamp_count)), config)

            output_dust_count = 1
            slpmsg_output_size = slpmsg_output_min_size
            postage_amt = rate * stamp_count
            total_coin_value = 0
            for coin in coins:
                total_coin_value += coin["token_value"]
                wallet.add_input_info(coin)
            change_amt = total_coin_value - send_amount - postage_amt

            if postage_amt > 0 and change_amt > 0:
                output_dust_count = 3
                slpmsg_output_size = slpmsg_output_max_size
            elif postage_amt > 0 or change_amt > 0:
                output_dust_count = 2
                slpmsg_output_size = slpmsg_output_mid_size

            txn_size_wo_stamps = txn_overhead + input_unit_size_ecdsa * len(
                coins
            ) + output_unit_size * output_dust_count + slpmsg_output_size

            # output cost differential (positive value means we need stamps)
            output_sats_diff = (output_dust_count * 546) - (len(coins) * 546)

            # fee cost differential (positive value means we need more stamps)
            fee_rate = 1
            sats_diff_w_fee = (txn_size_wo_stamps * fee_rate
                               ) + output_sats_diff - stamp_count * weight

        if output_dust_count == 1:
            amts = [send_amount]
            needs_postage = False
        elif output_dust_count == 2 and postage_amt > 0:
            amts = [send_amount, postage_amt]
            needs_postage = True
        elif output_dust_count == 2 and change_amt > 0:
            amts = [send_amount, change_amt]
            needs_postage = False
        elif output_dust_count == 3:
            amts = [send_amount, postage_amt, change_amt]
            needs_postage = True
        else:
            raise Exception("Unhandled exception")

        slp_output = buildSendOpReturnOutput_V1(tokenId, amts)

        return coins, slp_output, needs_postage, postage_amt
    def prepare_nft_parent(self, preview=False):

        self.show_message(
            "An initial preparation transaction is required before a new NFT can be created. This ensures only 1 parent token is burned in the NFT Genesis transaction.\n\nAfter this is transaction is broadcast you can proceed to fill out the NFT details and then click 'Create NFT'."
        )

        # IMPORTANT: set wallet.send_slpTokenId to None to guard tokens during this transaction
        self.main_window.token_type_combo.setCurrentIndex(0)
        assert self.main_window.slp_token_id == None

        coins = self.main_window.get_coins()
        fee = None

        try:
            selected_coin = None
            nft_parent_coins = self.main_window.wallet.get_slp_utxos(
                self.nft_parent_id,
                domain=None,
                exclude_frozen=True,
                confirmed_only=self.main_window.config.get(
                    'confirmed_only', False),
                slp_include_invalid=False,
                slp_include_baton=False)
            for coin in nft_parent_coins:
                if coin['token_value'] > 1:
                    selected_coin = coin
                    break

            if selected_coin['token_value'] < 19:
                slp_qtys = [1] * selected_coin['token_value']
            elif selected_coin['token_value'] >= 19:
                slp_qtys = [1] * 18
                slp_qtys.append(selected_coin['token_value'] - 18)
            outputs = []
            try:
                slp_op_return_msg = buildSendOpReturnOutput_V1(
                    self.nft_parent_id, slp_qtys, token_type=129)
                outputs.append(slp_op_return_msg)
            except OPReturnTooLarge:
                self.show_message(
                    _("Optional string text causiing OP_RETURN greater than 223 bytes."
                      ))
                return
            except Exception as e:
                traceback.print_exc(file=sys.stdout)
                self.show_message(str(e))
                return
            try:
                addr = self.parse_address(self.token_pay_to_e.text())
                for i in slp_qtys:
                    outputs.append((TYPE_ADDRESS, addr, 546))
            except:
                self.show_message(
                    _("Must have Receiver Address in simpleledger format."))
                return
            if selected_coin:
                tx = self.main_window.wallet.make_unsigned_transaction(
                    coins,
                    outputs,
                    self.main_window.config,
                    fee,
                    None,
                    mandatory_coins=[selected_coin])
            else:
                self.show_message(
                    _("Unable to select a parent coin to prepare."))
                return
        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)
            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:
                    token_id = tx.txid()
                    if self.token_name_e.text() == '':
                        wallet_name = tx.txid()[0:5]
                    else:
                        wallet_name = self.token_name_e.text()[0:20]
                    # Check for duplication error
                    d = self.wallet.token_types.get(token_id)
                    for tid, d in self.wallet.token_types.items():
                        if d['name'] == wallet_name and tid != token_id:
                            wallet_name = wallet_name + "-" + token_id[:3]
                            break
                    self.broadcast_transaction(tx,
                                               self.token_name_e.text(),
                                               wallet_name,
                                               is_nft_prep=True)
                    self.token_name_e.setDisabled(False)
                    self.token_ticker_e.setDisabled(False)
                    self.token_url_e.setDisabled(False)
                    self.token_dochash_e.setDisabled(False)
                    self.token_pay_to_e.setDisabled(False)
                    self.token_baton_to_e.setDisabled(False)
                    self.warn1.setHidden(True)
                    self.warn2.setHidden(True)
                    self.warn_msg.setHidden(True)

        self.sign_tx_with_password(tx, sign_done, password)