From 590c56ec8581e100b2870850fdecb10fbeb42d1b Mon Sep 17 00:00:00 2001 From: vtexier <vit@free.fr> Date: Fri, 27 Mar 2020 18:34:19 +0100 Subject: [PATCH] [enh] #798 add context_menu "Send as source" and handle source in transfer GUI --- src/sakia/data/processors/sources.py | 3 + .../gui/navigation/txhistory/controller.py | 10 +- src/sakia/gui/sub/password_input/view.py | 4 + src/sakia/gui/sub/transfer/controller.py | 132 ++++++++------ src/sakia/gui/sub/transfer/model.py | 13 +- src/sakia/gui/sub/transfer/transfer.ui | 166 ++++++++++++++++-- src/sakia/gui/widgets/context_menu.py | 29 ++- src/sakia/services/documents.py | 56 +++--- src/sakia/services/sources.py | 3 + 9 files changed, 320 insertions(+), 96 deletions(-) diff --git a/src/sakia/data/processors/sources.py b/src/sakia/data/processors/sources.py index 4750ff56..0ca1812d 100644 --- a/src/sakia/data/processors/sources.py +++ b/src/sakia/data/processors/sources.py @@ -35,6 +35,9 @@ class SourcesProcessor: except sqlite3.IntegrityError: self._logger.debug("Source already known: {0}".format(source.identifier)) + def get_one(self, **search): + return self._repo.get_one(**search) + def amount(self, currency, pubkey): """ Get the amount value of the sources for a given pubkey diff --git a/src/sakia/gui/navigation/txhistory/controller.py b/src/sakia/gui/navigation/txhistory/controller.py index f30362b0..2bbb639e 100644 --- a/src/sakia/gui/navigation/txhistory/controller.py +++ b/src/sakia/gui/navigation/txhistory/controller.py @@ -55,8 +55,8 @@ class TxHistoryController(QObject): sources_service, ): - transfer = TransferController.integrate_to_main_view(None, app, connection) - view = TxHistoryView(parent.view, transfer.view) + controller = TransferController.integrate_to_main_view(None, app, connection) + view = TxHistoryView(parent.view, controller.view) model = TxHistoryModel( None, app, @@ -66,14 +66,14 @@ class TxHistoryController(QObject): transactions_service, sources_service, ) - txhistory = cls(view, model, transfer) + txhistory = cls(view, model, controller) model.setParent(txhistory) app.referential_changed.connect(txhistory.refresh_balance) app.sources_refreshed.connect(txhistory.refresh_balance) txhistory.view_in_wot.connect(app.view_in_wot) txhistory.view.spin_page.valueChanged.connect(model.change_page) - transfer.accepted.connect(view.clear) - transfer.rejected.connect(view.clear) + controller.accepted.connect(view.clear) + controller.rejected.connect(view.clear) return txhistory def refresh_minimum_maximum(self): diff --git a/src/sakia/gui/sub/password_input/view.py b/src/sakia/gui/sub/password_input/view.py index 97ba9bdd..0e016bd6 100644 --- a/src/sakia/gui/sub/password_input/view.py +++ b/src/sakia/gui/sub/password_input/view.py @@ -20,6 +20,10 @@ class PasswordInputView(QWidget, Ui_PasswordInputWidget): self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) self.layout().addWidget(self.button_box) self.button_box.hide() + font = self.edit_secret_key.font() + font.setBold(False) + self.edit_secret_key.setFont(font) + self.edit_password.setFont(font) def error(self, text): self.label_info.setText(text) diff --git a/src/sakia/gui/sub/transfer/controller.py b/src/sakia/gui/sub/transfer/controller.py index e2d64c04..1528e3cf 100644 --- a/src/sakia/gui/sub/transfer/controller.py +++ b/src/sakia/gui/sub/transfer/controller.py @@ -70,51 +70,69 @@ class TransferController(QObject): password_input.view, ) model = TransferModel(app) - transfer = cls(view, model, search_user, user_information, password_input) + controller = cls(view, model, search_user, user_information, password_input) search_user.identity_selected.connect(user_information.search_identity) - view.set_keys(transfer.model.available_connections()) - view.set_contacts(transfer.model.contacts()) + view.set_keys(controller.model.available_connections()) + view.set_contacts(controller.model.contacts()) app.new_connection.connect(view.add_key) app.connection_removed.connect(view.remove_key) - return transfer + return controller @classmethod def integrate_to_main_view(cls, parent, app, connection): - transfer = cls.create(parent, app) - transfer.view.combo_connections.setCurrentText(connection.title()) - transfer.view.radio_pubkey.toggle() - transfer.view.groupbox_connection.hide() - transfer.view.label_total.hide() - return transfer + controller = cls.create(parent, app) + controller.view.combo_connections.setCurrentText(connection.title()) + controller.view.radio_pubkey.toggle() + controller.view.label_connections.hide() + controller.view.combo_connections.hide() + controller.view.label_total.hide() + return controller @classmethod - def open_transfer_with_pubkey(cls, parent, app, connection, pubkey): - transfer = cls.create(parent, app) - transfer.view.groupbox_connection.show() + def open_transfer_with_pubkey(cls, parent, app, connection, pubkey, source): + controller = cls.create(parent, app) + controller.view.label_connections.show() + controller.view.combo_connections.show() if connection: - transfer.view.combo_connections.setCurrentText(connection.title()) - transfer.view.edit_pubkey.setText(pubkey) - transfer.view.radio_pubkey.setChecked(True) - transfer.view.radio_pubkey.toggle() - - transfer.refresh() - return transfer + controller.view.combo_connections.setCurrentText(connection.title()) + if pubkey: + controller.view.edit_pubkey.setText(pubkey) + controller.view.radio_pubkey.setChecked(True) + controller.view.radio_pubkey.toggle() + else: + controller.view.radio_local_key.setChecked(True) + controller.view.radio_local_key.toggle() + if source: + controller.model.current_source = source + controller.view.label_source_identifier.setText( + "{}:{}".format(source.identifier, source.noffset) + ) + amount = source.amount * (10 ** source.base) / 100 + controller.view.spinbox_amount.setValue(amount) + controller.view.spinbox_amount.setDisabled(True) + controller.view.spinbox_relative.setDisabled(True) + controller.view.button_source_check.setEnabled(True) + + controller.refresh() + return controller @classmethod - def send_money_to_pubkey(cls, parent, app, connection, pubkey): + def send_money_to_pubkey(cls, parent, app, connection, pubkey, source): dialog = QDialog(parent) dialog.setWindowTitle( QCoreApplication.translate("TransferController", "Transfer") ) dialog.setLayout(QVBoxLayout(dialog)) - transfer = cls.open_transfer_with_pubkey(parent, app, connection, pubkey) + controller = cls.open_transfer_with_pubkey( + parent, app, connection, pubkey, source + ) - dialog.layout().addWidget(transfer.view) - transfer.accepted.connect(dialog.accept) - transfer.rejected.connect(dialog.reject) + dialog.layout().addWidget(controller.view) + controller.accepted.connect(dialog.accept) + controller.rejected.connect(dialog.reject) return dialog.exec() @classmethod @@ -124,14 +142,14 @@ class TransferController(QObject): QCoreApplication.translate("TransferController", "Transfer") ) dialog.setLayout(QVBoxLayout(dialog)) - transfer = cls.open_transfer_with_pubkey( - parent, app, connection, identity.pubkey + controller = cls.open_transfer_with_pubkey( + parent, app, connection, identity.pubkey, None ) - transfer.user_information.change_identity(identity) - dialog.layout().addWidget(transfer.view) - transfer.accepted.connect(dialog.accept) - transfer.rejected.connect(dialog.reject) + controller.user_information.change_identity(identity) + dialog.layout().addWidget(controller.view) + controller.accepted.connect(dialog.accept) + controller.rejected.connect(dialog.reject) return dialog.exec() @classmethod @@ -141,34 +159,35 @@ class TransferController(QObject): QCoreApplication.translate("TransferController", "Transfer") ) dialog.setLayout(QVBoxLayout(dialog)) - transfer = cls.create(parent, app) - transfer.view.groupbox_connection.show() - transfer.view.label_total.show() - transfer.view.combo_connections.setCurrentText(connection.title()) - transfer.view.edit_pubkey.setText(resent_transfer.receivers[0]) - transfer.view.radio_pubkey.setChecked(True) + controller = cls.create(parent, app) + controller.view.label_connections.show() + controller.view.combo_connections.show() + controller.view.label_total.show() + controller.view.combo_connections.setCurrentText(connection.title()) + controller.view.edit_pubkey.setText(resent_transfer.receivers[0]) + controller.view.radio_pubkey.setChecked(True) - transfer.refresh() + controller.refresh() - current_base = transfer.model.current_base() + current_base = controller.model.current_base() current_base_amount = resent_transfer.amount / pow( 10, resent_transfer.amount_base - current_base ) - relative = transfer.model.quant_to_rel(current_base_amount / 100) - transfer.view.set_spinboxes_parameters(current_base_amount / 100, relative) - transfer.view.change_relative_amount(relative) - transfer.view.change_quantitative_amount(current_base_amount / 100) + relative = controller.model.quant_to_rel(current_base_amount / 100) + controller.view.set_spinboxes_parameters(current_base_amount / 100, relative) + controller.view.change_relative_amount(relative) + controller.view.change_quantitative_amount(current_base_amount / 100) connections_processor = ConnectionsProcessor.instanciate(app) wallet_index = connections_processor.connections().index(connection) - transfer.view.combo_connections.setCurrentIndex(wallet_index) - transfer.view.edit_pubkey.setText(resent_transfer.receivers[0]) - transfer.view.radio_pubkey.toggle() - transfer.view.edit_message.setText(resent_transfer.comment) - dialog.layout().addWidget(transfer.view) - transfer.accepted.connect(dialog.accept) - transfer.rejected.connect(dialog.reject) + controller.view.combo_connections.setCurrentIndex(wallet_index) + controller.view.edit_pubkey.setText(resent_transfer.receivers[0]) + controller.view.radio_pubkey.toggle() + controller.view.edit_message.setText(resent_transfer.comment) + dialog.layout().addWidget(controller.view) + controller.accepted.connect(dialog.accept) + controller.rejected.connect(dialog.reject) return dialog.exec() def valid_crc_pubkey(self): @@ -215,7 +234,8 @@ class TransferController(QObject): async def accept(self): logging.debug("Accept transfer action...") self.view.button_box.setEnabled(False) - comment = self.view.edit_message.text() + + source = self.model.current_source logging.debug("checking recipient mode...") recipient = self.selected_pubkey() @@ -228,11 +248,19 @@ class TransferController(QObject): logging.debug("Setting cursor...") QApplication.setOverrideCursor(Qt.WaitCursor) + comment = self.view.edit_message.text() lock_mode = self.view.combo_locks.currentIndex() logging.debug("Send money...") result, transactions = await self.model.send_money( - recipient, secret_key, password, amount, amount_base, comment, lock_mode + recipient, + secret_key, + password, + amount, + amount_base, + comment, + lock_mode, + source, ) if result[0]: await self.view.show_success(self.model.notifications(), recipient) diff --git a/src/sakia/gui/sub/transfer/model.py b/src/sakia/gui/sub/transfer/model.py index 071c02bc..7b2c632d 100644 --- a/src/sakia/gui/sub/transfer/model.py +++ b/src/sakia/gui/sub/transfer/model.py @@ -21,6 +21,7 @@ class TransferModel(QObject): app = attr.ib() connection = attr.ib(default=None) resent_transfer = attr.ib(default=None) + current_source = attr.ib(default=None) _blockchain_processor = attr.ib(default=None) _sources_processor = attr.ib(default=None) _connections_processor = attr.ib(default=None) @@ -90,7 +91,15 @@ class TransferModel(QObject): self.connection = connections[index] async def send_money( - self, recipient, secret_key, password, amount, amount_base, comment, lock_mode + self, + recipient, + secret_key, + password, + amount, + amount_base, + comment, + lock_mode, + source, ): """ Send money to given recipient using the account @@ -102,6 +111,7 @@ class TransferModel(QObject): :param int amount_base: :param str comment: :param int lock_mode: + :param Source source: :return: the result of the send """ @@ -114,6 +124,7 @@ class TransferModel(QObject): amount_base, comment, lock_mode, + source, ) for transaction in transactions: self.app.sources_service.parse_transaction_outputs( diff --git a/src/sakia/gui/sub/transfer/transfer.ui b/src/sakia/gui/sub/transfer/transfer.ui index 8a5da344..7c201ad5 100644 --- a/src/sakia/gui/sub/transfer/transfer.ui +++ b/src/sakia/gui/sub/transfer/transfer.ui @@ -15,22 +15,98 @@ </property> <layout class="QVBoxLayout" name="verticalLayout"> <item> - <widget class="QGroupBox" name="groupbox_connection"> - <property name="title"> - <string>Select account</string> + <layout class="QHBoxLayout" name="layout_connections"> + <property name="sizeConstraint"> + <enum>QLayout::SetDefaultConstraint</enum> </property> - <layout class="QHBoxLayout" name="horizontalLayout_4"> - <item> - <widget class="QComboBox" name="combo_connections"/> - </item> - </layout> - </widget> + <item> + <widget class="QLabel" name="label_connections"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>Select account</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="combo_connections"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <property name="spacing"> + <number>6</number> + </property> + <item> + <widget class="QLabel" name="label_source"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>Source</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_source_identifier"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Automatic</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_source_check"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Check</string> + </property> + </widget> + </item> + </layout> </item> <item> <widget class="QGroupBox" name="group_box_recipient"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> <property name="title"> <string>Transfer money to</string> </property> + <property name="flat"> + <bool>false</bool> + </property> <layout class="QHBoxLayout" name="horizontalLayout_5"> <item> <layout class="QVBoxLayout" name="verticalLayout_4"> @@ -50,6 +126,12 @@ <verstretch>0</verstretch> </sizepolicy> </property> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> <property name="text"> <string>&Recipient public key</string> </property> @@ -85,6 +167,12 @@ <verstretch>0</verstretch> </sizepolicy> </property> + <property name="font"> + <font> + <weight>50</weight> + <bold>false</bold> + </font> + </property> <property name="inputMask"> <string/> </property> @@ -147,7 +235,14 @@ </widget> </item> <item> - <widget class="QComboBox" name="combo_local_keys"/> + <widget class="QComboBox" name="combo_local_keys"> + <property name="font"> + <font> + <weight>50</weight> + <bold>false</bold> + </font> + </property> + </widget> </item> </layout> </item> @@ -164,7 +259,14 @@ </widget> </item> <item> - <widget class="QComboBox" name="combo_contacts"/> + <widget class="QComboBox" name="combo_contacts"> + <property name="font"> + <font> + <weight>50</weight> + <bold>false</bold> + </font> + </property> + </widget> </item> </layout> </item> @@ -184,6 +286,12 @@ <layout class="QVBoxLayout" name="verticalLayout_8"> <item> <widget class="QLabel" name="label_total"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> <property name="text"> <string>Available money: </string> </property> @@ -193,6 +301,12 @@ <layout class="QHBoxLayout" name="horizontalLayout_3"> <item> <widget class="QLabel" name="label_3"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> <property name="text"> <string>Amount</string> </property> @@ -251,6 +365,12 @@ <layout class="QHBoxLayout" name="horizontalLayout_7"> <item> <widget class="QGroupBox" name="groupBox_3"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> <property name="title"> <string>Message</string> </property> @@ -273,12 +393,24 @@ <verstretch>0</verstretch> </sizepolicy> </property> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> <property name="title"> <string>Spend condition</string> </property> <layout class="QHBoxLayout" name="horizontalLayout_9"> <item> <widget class="QComboBox" name="combo_locks"> + <property name="font"> + <font> + <weight>50</weight> + <bold>false</bold> + </font> + </property> <item> <property name="text"> <string>Receiver signature</string> @@ -298,6 +430,18 @@ </item> <item> <widget class="QGroupBox" name="groupBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> <property name="title"> <string>Secret Key / Password</string> </property> diff --git a/src/sakia/gui/widgets/context_menu.py b/src/sakia/gui/widgets/context_menu.py index 6f01348d..d7f6709f 100644 --- a/src/sakia/gui/widgets/context_menu.py +++ b/src/sakia/gui/widgets/context_menu.py @@ -5,7 +5,9 @@ from PyQt5.QtWidgets import QMenu, QAction, QApplication, QMessageBox from duniterpy.constants import PUBKEY_REGEX from duniterpy.documents.crc_pubkey import CRCPubkey -from sakia.data.entities import Identity, Transaction, Dividend + +from sakia.app import Application +from sakia.data.entities import Identity, Transaction, Dividend, Connection from sakia.data.processors import BlockchainProcessor, TransactionsProcessor from sakia.decorators import asyncify from sakia.gui.sub.certification.controller import CertificationController @@ -17,7 +19,7 @@ class ContextMenu(QObject): view_identity_in_wot = pyqtSignal(object) identity_information_loaded = pyqtSignal(Identity) - def __init__(self, qmenu, app, connection): + def __init__(self, qmenu: QMenu, app: Application, connection: Connection): """ :param PyQt5.QtWidgets.QMenu: the qmenu widget :param sakia.app.Application app: Application instance @@ -25,7 +27,7 @@ class ContextMenu(QObject): """ super().__init__() self.qmenu = qmenu - self._app = app + self._app = app # type: Application self._connection = connection @staticmethod @@ -130,6 +132,16 @@ class ContextMenu(QObject): ) menu.qmenu.addAction(cancel) + # if special lock condition on transaction... + if transfer.conditions is not None: + send_as_source = QAction( + QCoreApplication.translate("ContextMenu", "Send as source"), + menu.qmenu.parent(), + ) + send_as_source.triggered.connect( + lambda checked, tr=transfer: menu.send_as_source(tr) + ) + menu.qmenu.addAction(send_as_source) if menu._app.parameters.expert_mode: copy_doc = QAction( QCoreApplication.translate( @@ -250,7 +262,7 @@ class ContextMenu(QObject): ) else: TransferController.send_money_to_pubkey( - None, self._app, self._connection, identity_or_pubkey + None, self._app, self._connection, identity_or_pubkey, None ) def view_wot(self, identity): @@ -286,6 +298,15 @@ This money transfer will be removed and not sent.""", self._app.db.commit() self._app.transaction_state_changed.emit(transfer) + def send_as_source(self, transfer: Transaction): + # get source from conditions and transaction hash + source = self._app.sources_service.get_one( + identifier=transfer.sha_hash, conditions=transfer.conditions + ) + TransferController.send_money_to_pubkey( + None, self._app, self._connection, None, source + ) + def copy_transaction_to_clipboard(self, tx): clipboard = QApplication.clipboard() clipboard.setText(tx.raw) diff --git a/src/sakia/services/documents.py b/src/sakia/services/documents.py index e2805005..afab7abe 100644 --- a/src/sakia/services/documents.py +++ b/src/sakia/services/documents.py @@ -494,10 +494,10 @@ class DocumentsService: message, currency, lock_mode=0, + source=None, ): """ Prepare a simple Transaction document - :param int lock_mode: Lock condition mode selected in combo box :param SigningKey key: the issuer of the transaction :param str receiver: the target of the transaction :param duniterpy.documents.BlockUID blockstamp: the blockstamp @@ -505,32 +505,39 @@ class DocumentsService: :param int amount_base: the amount base of the currency :param str message: the comment of the tx :param str currency: the target community + :param int lock_mode: Lock condition mode selected in combo box + :param Source source: Source instance or None :return: the transaction document :rtype: List[sakia.data.entities.Transaction] """ forged_tx = [] - sources = [None] * 41 - while len(sources) > 40: - result = self.tx_sources(int(amount), amount_base, currency, key) - sources = result[0] - computed_outputs = result[1] - overheads = result[2] - # Fix issue #594 - if len(sources) > 40: - sources_value = 0 - for s in sources[:39]: - sources_value += s.amount * (10 ** s.base) - sources_value, sources_base = reduce_base(sources_value, 0) - chained_tx = self.prepare_tx( - key, - key.pubkey, - blockstamp, - sources_value, - sources_base, - "[CHAINED]", - currency, - ) - forged_tx += chained_tx + if source is None: + # automatic selection of sources + sources = [None] * 41 + while len(sources) > 40: + result = self.tx_sources(int(amount), amount_base, currency, key) + sources = result[0] + computed_outputs = result[1] + overheads = result[2] + # Fix issue #594 + if len(sources) > 40: + sources_value = 0 + for s in sources[:39]: + sources_value += s.amount * (10 ** s.base) + sources_value, sources_base = reduce_base(sources_value, 0) + chained_tx = self.prepare_tx( + key, + key.pubkey, + blockstamp, + sources_value, + sources_base, + "[CHAINED]", + currency, + ) + forged_tx += chained_tx + else: + sources = [source] + logging.debug("Inputs: {0}".format(sources)) inputs = self.tx_inputs(sources) @@ -588,9 +595,11 @@ class DocumentsService: amount_base, message, lock_mode, + source, ): """ Send money to a given recipient in a specified community + :param Source source: Source instance or None :param int lock_mode: Index in the combo_locks combobox :param sakia.data.entities.Connection connection: The account salt :param str secret_key: The account secret_key @@ -617,6 +626,7 @@ class DocumentsService: message, connection.currency, lock_mode, + source, ) for i, tx in enumerate(tx_entities): diff --git a/src/sakia/services/sources.py b/src/sakia/services/sources.py index a13e7798..be5ce7a3 100644 --- a/src/sakia/services/sources.py +++ b/src/sakia/services/sources.py @@ -43,6 +43,9 @@ class SourcesServices(QObject): self.currency = currency self._logger = logging.getLogger("sakia") + def get_one(self, **search): + return self._sources_processor.get_one(**search) + def amount(self, pubkey): return self._sources_processor.amount(self.currency, pubkey) -- GitLab