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()
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)