From 98abc7e55c6330fc420e8adf39db3095fdd68b0e Mon Sep 17 00:00:00 2001 From: inso <insomniak.fr@gmaiL.com> Date: Fri, 30 Dec 2016 10:46:45 +0100 Subject: [PATCH] Implement transfer dialog --- src/sakia/app.py | 5 +- src/sakia/data/entities/blockchain.py | 2 +- src/sakia/data/entities/connection.py | 2 +- src/sakia/data/entities/transaction.py | 41 +++- src/sakia/data/processors/connections.py | 4 +- src/sakia/data/processors/sources.py | 25 ++- src/sakia/data/processors/transactions.py | 27 +-- src/sakia/data/processors/tx_lifecycle.py | 24 +- .../dialogs/certification/certification.ui | 15 +- .../gui/dialogs/certification/controller.py | 30 +-- src/sakia/gui/dialogs/certification/model.py | 12 +- src/sakia/gui/dialogs/certification/view.py | 5 - src/sakia/gui/dialogs/transfer/controller.py | 150 ++++++------- src/sakia/gui/dialogs/transfer/model.py | 104 ++++----- src/sakia/gui/dialogs/transfer/transfer.py | 2 +- src/sakia/gui/dialogs/transfer/transfer.ui | 81 +------ src/sakia/gui/dialogs/transfer/view.py | 40 +--- src/sakia/gui/sub/user_information/model.py | 5 +- src/sakia/services/documents.py | 211 +++++++++++++++++- src/sakia/tests/conftest.py | 12 +- .../identities_tab/test_identities_table.py | 2 +- .../functional/test_certification_dialog.py | 2 +- .../tests/functional/test_transfer_dialog.py | 32 +++ .../functional/transfer/test_transfer.py | 89 -------- 24 files changed, 477 insertions(+), 445 deletions(-) create mode 100644 src/sakia/tests/functional/test_transfer_dialog.py delete mode 100644 src/sakia/tests/functional/transfer/test_transfer.py diff --git a/src/sakia/app.py b/src/sakia/app.py index 49e1d2aa..43edddd1 100644 --- a/src/sakia/app.py +++ b/src/sakia/app.py @@ -13,6 +13,7 @@ from sakia.data.connectors import BmaConnector from sakia.services import NetworkService, BlockchainService, IdentitiesService, \ SourcesServices, TransactionsService, DocumentsService from sakia.data.repositories import SakiaDatabase +from sakia.data.entities import Transaction from sakia.data.processors import BlockchainProcessor, NodesProcessor, IdentitiesProcessor, \ CertificationsProcessor, SourcesProcessor, TransactionsProcessor, ConnectionsProcessor from sakia.data.files import AppDataFile, UserParametersFile @@ -43,6 +44,8 @@ class Application(QObject): :param sakia.services.DocumentsService documents_service: A service to broadcast documents """ + new_transfer = pyqtSignal(Transaction) + qapp = attr.ib() loop = attr.ib() options = attr.ib() @@ -102,7 +105,7 @@ class Application(QObject): self.identities_services = {} self.sources_services = {} self.transactions_services = {} - self.documents_service = DocumentsService(bma_connector, blockchain_processor, identities_processor) + self.documents_service = DocumentsService.instanciate(self) for currency in self.db.connections_repo.get_currencies(): if currency not in self.identities_services: diff --git a/src/sakia/data/entities/blockchain.py b/src/sakia/data/entities/blockchain.py index 7d5b3418..915551f2 100644 --- a/src/sakia/data/entities/blockchain.py +++ b/src/sakia/data/entities/blockchain.py @@ -57,7 +57,7 @@ class Blockchain: # Last members count last_members_count = attr.ib(convert=int, default=0, cmp=False, hash=False) # Last UD amount in units (multiply by 10^base) - last_ud = attr.ib(convert=int, default=0, cmp=False, hash=False) + last_ud = attr.ib(convert=int, default=1, cmp=False, hash=False) # Last UD base last_ud_base = attr.ib(convert=int, default=0, cmp=False, hash=False) # Last UD base diff --git a/src/sakia/data/entities/connection.py b/src/sakia/data/entities/connection.py index 15b9c2ce..79d23da9 100644 --- a/src/sakia/data/entities/connection.py +++ b/src/sakia/data/entities/connection.py @@ -21,7 +21,7 @@ class Connection: password = attr.ib(init=False, convert=str, default="", cmp=False, hash=False) def title(self): - return self.uid + " - " + self.pubkey[:5] + return self.uid + "[" + self.pubkey[:7] + "]@" + self.currency @property def scrypt_params(self): diff --git a/src/sakia/data/entities/transaction.py b/src/sakia/data/entities/transaction.py index 3f79dcb4..e89fadef 100644 --- a/src/sakia/data/entities/transaction.py +++ b/src/sakia/data/entities/transaction.py @@ -59,9 +59,26 @@ def parse_transaction_doc(tx_doc, pubkey, block_number, mediantime, txid): return transaction return None + @attr.s() class Transaction: + """ + Transaction entity + :param str currency: the currency of the transaction + :param str sha_hash: the hash of the transaction + :param int written_block: the number of the block + :param str blockstamp: the blockstamp of the transaction + :param int timestamp: the timestamp of the transaction + :param str signature: the signature + :param str issuer: the pubkey of the issuer + :param str receiver: the pubkey of the receiver + :param int amount: the amount + :param str amount_base: the amount base + :param str comment: a comment + :param str txid: the transaction id to sort transctions + :param str state: the state of the transaction + """ TO_SEND = 0 AWAITING = 1 VALIDATING = 2 @@ -70,19 +87,19 @@ class Transaction: DROPPED = 16 LOCAL = 128 - currency = attr.ib(convert=str, cmp=False) - sha_hash = attr.ib(convert=str) + currency = attr.ib(convert=str, cmp=False) + sha_hash = attr.ib(convert=str) written_block = attr.ib(convert=int, cmp=False) - blockstamp = attr.ib(convert=block_uid, cmp=False) - timestamp = attr.ib(convert=int, cmp=False) - signature = attr.ib(convert=str, cmp=False) - issuer = attr.ib(convert=str, cmp=False) - receiver = attr.ib(convert=str, cmp=False) - amount = attr.ib(convert=int, cmp=False) - amount_base = attr.ib(convert=int, cmp=False) - comment = attr.ib(convert=str, cmp=False) - txid = attr.ib(convert=int, cmp=False) - state = attr.ib(convert=int, cmp=False) + blockstamp = attr.ib(convert=block_uid, cmp=False) + timestamp = attr.ib(convert=int, cmp=False) + signature = attr.ib(convert=str, cmp=False) + issuer = attr.ib(convert=str, cmp=False) + receiver = attr.ib(convert=str, cmp=False) + amount = attr.ib(convert=int, cmp=False) + amount_base = attr.ib(convert=int, cmp=False) + comment = attr.ib(convert=str, cmp=False) + txid = attr.ib(convert=int, cmp=False) + state = attr.ib(convert=int, cmp=False) @property def local(self): diff --git a/src/sakia/data/processors/connections.py b/src/sakia/data/processors/connections.py index 536b4d67..db120052 100644 --- a/src/sakia/data/processors/connections.py +++ b/src/sakia/data/processors/connections.py @@ -29,8 +29,8 @@ class ConnectionsProcessor: def pubkeys(self): return self._connections_repo.get_pubkeys() - def connections(self, currency): - return self._connections_repo.get_all(currency=currency) + def connections(self): + return self._connections_repo.get_all() def currencies(self): return self._connections_repo.get_currencies() diff --git a/src/sakia/data/processors/sources.py b/src/sakia/data/processors/sources.py index 0d43d4af..5477ff7a 100644 --- a/src/sakia/data/processors/sources.py +++ b/src/sakia/data/processors/sources.py @@ -9,8 +9,12 @@ import asyncio @attr.s class SourcesProcessor: - _repo = attr.ib() # :type sakia.data.repositories.SourcesRepo - _bma_connector = attr.ib() # :type sakia.data.connectors.bma.BmaConnector + """ + :param sakia.data.repositories.SourcesRepo _repo: the repository of the sources + :param sakia.data.connectors.bma.BmaConnector _bma_connector: the bma connector + """ + _repo = attr.ib() + _bma_connector = attr.ib() @classmethod def instanciate(cls, app): @@ -55,3 +59,20 @@ class SourcesProcessor: """ sources = self._repo.get_all(currency=currency, pubkey=pubkey) return sum([s.amount * (10**s.base) for s in sources]) + + def available(self, currency): + """" + :param str currency: the currency of the sources + :rtype: list[sakia.data.entities.Source] + """ + return self._repo.get_all(currency=currency) + + def consume(self, sources): + """ + + :param currency: + :param sources: + :return: + """ + for s in sources: + self._repo.drop(s) \ No newline at end of file diff --git a/src/sakia/data/processors/transactions.py b/src/sakia/data/processors/transactions.py index 19bcb0a2..25b83990 100644 --- a/src/sakia/data/processors/transactions.py +++ b/src/sakia/data/processors/transactions.py @@ -26,6 +26,15 @@ class TransactionsProcessor: return cls(app.db.transactions_repo, BmaConnector(NodesProcessor(app.db.nodes_repo))) + def next_txid(self, currency, block_number): + """ + :param str currency: + :param str block_number: + :rtype: int + """ + transfers = self._repo.get_all(currency=currency, written_on=block_number) + return max([tx.txid for tx in transfers]) if transfers else 0 + def transfers(self, currency, pubkey): """ Get all transfers from or to a given pubkey @@ -94,7 +103,7 @@ class TransactionsProcessor: """ self.run_state_transitions(tx, ()) - async def send(self, tx, txdoc, community): + async def send(self, tx, txdoc, currency): """ Send a transaction and update the transfer state to AWAITING if accepted. If the transaction was refused (return code != 200), state becomes REFUSED @@ -102,16 +111,9 @@ class TransactionsProcessor: :param sakia.data.entities.Transaction tx: the transaction :param txdoc: A transaction duniterpy object - :param community: The community target of the transaction - """ - tx.sha_hash = txdoc.sha_hash - responses = await community.bma_access.broadcast(bma.tx.process, - post_args={'transaction': txdoc.signed_raw()}) - blockUID = community.network.current_blockUID - block = await community.bma_access.future_request(bma.blockchain.block, - req_args={'number': blockUID.number}) - signed_raw = "{0}{1}\n".format(block['raw'], block['signature']) - block_doc = Block.from_signed_raw(signed_raw) + :param currency: The community target of the transaction + """ + responses = await self._bma_connector.broadcast(currency, bma.tx.process, req_args={'transaction': txdoc.signed_raw()}) result = (False, "") for r in responses: if r.status == 200: @@ -120,9 +122,8 @@ class TransactionsProcessor: result = (False, (await r.text())) else: await r.text() - self.run_state_transitions(tx, ([r.status for r in responses], block_doc)) self.run_state_transitions(tx, ([r.status for r in responses],)) - return result + return result, tx async def initialize_transactions(self, identity, log_stream): """ diff --git a/src/sakia/data/processors/tx_lifecycle.py b/src/sakia/data/processors/tx_lifecycle.py index df010d33..7769bab6 100644 --- a/src/sakia/data/processors/tx_lifecycle.py +++ b/src/sakia/data/processors/tx_lifecycle.py @@ -129,18 +129,6 @@ def _is_locally_created(tx): """ return tx.local - -def _wait(tx, current_block): - """ - Set the transfer as AWAITING confrmation. - - :param sakia.data.entities.Transaction tx: the transaction - :param duniterpy.documents.Block current_block: Current block of the main blockchain - """ - tx.blockstamp = current_block.blockUID - tx.timestamp = int(time.time()) - - def _be_validating(tx, block): """ Action when the transfer ins found in a block @@ -166,10 +154,10 @@ def _drop(tx): # keys are a tuple containg (current_state, transition_parameters) # values are tuples containing (transition_test, transition_success, new_state) states = { - (Transaction.TO_SEND, (list, Block)): + (Transaction.TO_SEND, (list,)): ( - (_broadcast_success, lambda tx, l, b: _wait(tx, b), Transaction.AWAITING), - (lambda tx, l, b: _broadcast_failure(tx, l), None, Transaction.REFUSED), + (_broadcast_success, None, Transaction.AWAITING), + (lambda tx, l: _broadcast_failure(tx, l), None, Transaction.REFUSED), ), (Transaction.TO_SEND, ()): ((_is_locally_created, _drop, Transaction.DROPPED),), @@ -187,10 +175,10 @@ states = { (Transaction.VALIDATED, (bool, Block, int)): ((_rollback_in_fork_window, lambda tx, r, b, i: _be_validating(tx, b), Transaction.VALIDATING),), - (Transaction.VALIDATED, (bool, Block)): + (Transaction.VALIDATED, (bool,)): ( - (_rollback_and_removed, lambda tx, r, b: _drop(tx), Transaction.DROPPED), - (_rollback_and_local, lambda tx, r, b: _wait(tx, b), Transaction.AWAITING), + (_rollback_and_removed, lambda tx, r: _drop(tx), Transaction.DROPPED), + (_rollback_and_local, None, Transaction.AWAITING), ), (Transaction.REFUSED, ()): diff --git a/src/sakia/gui/dialogs/certification/certification.ui b/src/sakia/gui/dialogs/certification/certification.ui index db5a007b..f67156ae 100644 --- a/src/sakia/gui/dialogs/certification/certification.ui +++ b/src/sakia/gui/dialogs/certification/certification.ui @@ -23,22 +23,9 @@ </sizepolicy> </property> <property name="title"> - <string>Send to currency network :</string> + <string>Select your identity</string> </property> <layout class="QVBoxLayout" name="verticalLayout_2"> - <item> - <widget class="QComboBox" name="combo_currency"/> - </item> - <item> - <widget class="QLabel" name="label"> - <property name="text"> - <string>With the following identity : </string> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - </widget> - </item> <item> <widget class="QComboBox" name="combo_pubkey"/> </item> diff --git a/src/sakia/gui/dialogs/certification/controller.py b/src/sakia/gui/dialogs/certification/controller.py index 2e408256..f7a8615a 100644 --- a/src/sakia/gui/dialogs/certification/controller.py +++ b/src/sakia/gui/dialogs/certification/controller.py @@ -28,7 +28,6 @@ class CertificationController(QObject): super().__init__() self.view.button_box.accepted.connect(self.accept) self.view.button_box.rejected.connect(self.reject) - self.view.combo_currency.currentIndexChanged.connect(self.change_currency) self.view.combo_pubkey.currentIndexChanged.connect(self.change_connection) @classmethod @@ -44,15 +43,13 @@ class CertificationController(QObject): model = CertificationModel(app) certification = cls(view, model, None, None) - search_user = SearchUserController.create(certification, app, model.available_currencies()[0]) + search_user = SearchUserController.create(certification, app, "") certification.set_search_user(search_user) - user_information = UserInformationController.create(certification, app, - model.available_currencies()[0], None) + user_information = UserInformationController.create(certification, app, "", None) certification.set_user_information(user_information) - view.set_currencies(certification.model.available_currencies()) - view.set_keys(certification.model.available_connections(certification.model.available_currencies()[0])) + view.set_keys(certification.model.available_connections()) return certification @classmethod @@ -67,7 +64,7 @@ class CertificationController(QObject): """ dialog = cls.create(parent, app) if connection: - dialog.view.combo_currency.setCurrentText(connection.currency) + dialog.view.combo_pubkey.setCurrentText(connection.title()) dialog.refresh() return dialog.exec() @@ -77,13 +74,12 @@ class CertificationController(QObject): Certify and identity :param sakia.gui.component.controller.ComponentController parent: the parent :param sakia.core.Application app: the application - :param sakia.core.Account account: the account certifying the identity - :param sakia.core.Community community: the community - :param sakia.core.registry.Identity identity: the identity certified + :param sakia.data.entities.Connection connection: the connection + :param sakia.data.entities.Identity identity: the identity certified :return: """ dialog = cls.create(parent, app) - dialog.view.combo_community.setCurrentText(connection.currency) + dialog.view.combo_pubkey.setCurrentText(connection.title()) dialog.refresh() return await dialog.async_exec() @@ -162,16 +158,10 @@ class CertificationController(QObject): """ self.user_information.search_identity(self.search_user.model.identity()) - def change_currency(self, index): - currency = self.model.available_currencies()[index] - connections = self.model.available_connections(currency) - self.view.set_selected_key(connections[0]) - self.search_user.set_currency(currency) - self.user_information.set_currency(currency) - def change_connection(self, index): - currency = self.model.available_currencies()[index] - self.model.set_connection(currency, index) + self.model.set_connection(index) + self.search_user.set_currency(self.model.connection.currency) + self.user_information.set_currency(self.model.connection.currency) self.refresh() def async_exec(self): diff --git a/src/sakia/gui/dialogs/certification/model.py b/src/sakia/gui/dialogs/certification/model.py index 3c3cbfaf..2c15e4ba 100644 --- a/src/sakia/gui/dialogs/certification/model.py +++ b/src/sakia/gui/dialogs/certification/model.py @@ -16,7 +16,6 @@ class CertificationModel(QObject): _certifications_processor = attr.ib(default=None) _identities_processor = attr.ib(default=None) _blockchain_processor = attr.ib(default=None) - _documents_service = attr.ib(default=None) def __attrs_post_init__(self): super().__init__() @@ -81,14 +80,11 @@ class CertificationModel(QObject): return is_member and self._blockchain_processor.current_buid(self.connection.currency) - def available_connections(self, currency): - return self._connections_processor.connections(currency=currency) + def available_connections(self): + return self._connections_processor.connections() - def available_currencies(self): - return self._connections_processor.currencies() - - def set_connection(self, currency, index): - connections = self._connections_processor.connections(currency=currency) + def set_connection(self, index): + connections = self._connections_processor.connections() self.connection = connections[index] def notification(self): diff --git a/src/sakia/gui/dialogs/certification/view.py b/src/sakia/gui/dialogs/certification/view.py index e6107f74..1ac5dc8a 100644 --- a/src/sakia/gui/dialogs/certification/view.py +++ b/src/sakia/gui/dialogs/certification/view.py @@ -54,11 +54,6 @@ class CertificationView(QDialog, Ui_CertificationDialog): """ self.combo_pubkey.setCurrentText(connection.title()) - def set_currencies(self, currencies): - self.combo_currency.clear() - for c in currencies: - self.combo_currency.addItem(c) - def set_search_user(self, search_user_view): """ diff --git a/src/sakia/gui/dialogs/transfer/controller.py b/src/sakia/gui/dialogs/transfer/controller.py index 45a0d389..c9a368a4 100644 --- a/src/sakia/gui/dialogs/transfer/controller.py +++ b/src/sakia/gui/dialogs/transfer/controller.py @@ -5,6 +5,7 @@ from PyQt5.QtCore import Qt, QObject from PyQt5.QtWidgets import QApplication from sakia.decorators import asyncify +from sakia.gui.password_asker import PasswordAskerDialog from sakia.gui.sub.search_user.controller import SearchUserController from sakia.gui.sub.user_information.controller import UserInformationController from .model import TransferModel @@ -16,26 +17,26 @@ class TransferController(QObject): The transfer component controller """ - def __init__(self, parent, view, model, search_user, user_information, password_asker): + def __init__(self, view, model, search_user, user_information): """ Constructor of the transfer component :param sakia.gui.transfer.view.TransferView: the view :param sakia.gui.transfer.model.TransferModel model: the model """ - super().__init__(parent) - self.password_asker = password_asker + super().__init__() + self.view = view + self.model = model self.search_user = search_user self.user_information = user_information self.view.button_box.accepted.connect(self.accept) self.view.button_box.rejected.connect(self.reject) - self.view.combo_community.currentIndexChanged.connect(self.change_current_community) - self.view.combo_wallets.currentIndexChanged.connect(self.change_current_wallet) + self.view.combo_connections.currentIndexChanged.connect(self.change_current_connection) self.view.spinbox_amount.valueChanged.connect(self.handle_amount_change) self.view.spinbox_relative.valueChanged.connect(self.handle_relative_change) @classmethod - def create(cls, parent, app, account, community, transfer, password_asker): + def create(cls, parent, app): """ Instanciate a transfer component :param sakia.gui.component.controller.ComponentController parent: @@ -43,54 +44,48 @@ class TransferController(QObject): :return: a new Transfer controller :rtype: TransferController """ - communities_names = [c.name for c in account.communities] - wallets_names = [w.name for w in account.wallets] - contacts_names = [c['name'] for c in account.contacts] + view = TransferView(parent.view if parent else None, None, None) + model = TransferModel(app) + transfer = cls(view, model, None, None) - view = TransferView(parent.view, None, None, communities_names, contacts_names, wallets_names) - model = TransferModel(None, app, account=account, community=community, resent_transfer=transfer) - transfer = cls(parent, view, model, None, None, password_asker) - - search_user = SearchUserController.create(transfer, app, - account=model.account, - community=model.community) + search_user = SearchUserController.create(transfer, app, "") transfer.set_search_user(search_user) - user_information = UserInformationController.create(transfer, app, - account=model.account, - community=model.community, - identity=None) + user_information = UserInformationController.create(transfer, app, "", None) transfer.set_user_information(user_information) - model.setParent(transfer) + + view.set_keys(transfer.model.available_connections()) return transfer @classmethod - def open_dialog(cls, parent, app, account, password_asker, community): - dialog = cls.create(parent, app, - account=account, - password_asker=password_asker, - community=community, - transfer=None) + def open_dialog(cls, parent, app, connection): + dialog = cls.create(parent, app) + if connection: + dialog.view.combo_currency.setCurrentText(connection.currency) + dialog.view.combo_wallets.setCurrentText(connection.title()) + dialog.refresh() return dialog.exec() @classmethod - async def send_money_to_identity(cls, parent, app, account, password_asker, community, identity): - dialog = cls.create(parent, app, - account=account, - password_asker=password_asker, - community=community, - transfer=None) + async def send_money_to_identity(cls, parent, app, connection, identity): + dialog = cls.create(parent, app) + dialog.view.combo_currency.setCurrentText(identity.currency) + dialog.view.combo_wallets.setCurrentText(connection.title()) dialog.view.edit_pubkey.setText(identity.pubkey) dialog.view.radio_pubkey.setChecked(True) + + dialog.refresh() return await dialog.async_exec() @classmethod - async def send_transfer_again(cls, parent, app, account, password_asker, community, resent_transfer): - dialog = cls.create(parent, app, - account=account, - password_asker=password_asker, - community=community, - resent_transfer=resent_transfer) + async def send_transfer_again(cls, parent, app, connection, resent_transfer): + dialog = cls.create(parent, app) + dialog.view.combo_currency.setCurrentText(resent_transfer.currency) + dialog.view.combo_wallets.setCurrentText(connection.title()) + dialog.view.edit_pubkey.setText(resent_transfer.receiver) + dialog.view.radio_pubkey.setChecked(True) + + dialog.refresh() relative = await dialog.model.quant_to_rel(resent_transfer.metadata['amount']) dialog.view.set_spinboxes_parameters(1, resent_transfer.metadata['amount'], relative) dialog.view.change_relative_amount(relative) @@ -105,14 +100,6 @@ class TransferController(QObject): return await dialog.async_exec() - @property - def view(self) -> TransferView: - return self._view - - @property - def model(self) -> TransferModel: - return self._model - def set_search_user(self, search_user): """ @@ -147,10 +134,7 @@ class TransferController(QObject): """ pubkey = None - if self.view.recipient_mode() == TransferView.RecipientMode.CONTACT: - contact_name = self.view.selected_contact() - pubkey = self.model.contact_name_pubkey(contact_name) - elif self.view.recipient_mode() == TransferView.RecipientMode.SEARCH: + if self.view.recipient_mode() == TransferView.RecipientMode.SEARCH: if self.search_user.current_identity(): pubkey = self.search_user.current_identity().pubkey else: @@ -166,9 +150,11 @@ class TransferController(QObject): logging.debug("checking recipient mode...") recipient = self.selected_pubkey() amount = self.view.spinbox_amount.value() + #TODO: Handle other amount base than 0 + amount_base = 0 logging.debug("Showing password dialog...") - password = await self.password_asker.async_exec() + password = await PasswordAskerDialog(self.model.connection).async_exec() if password == "": self.view.button_box.setEnabled(True) return @@ -177,18 +163,18 @@ class TransferController(QObject): QApplication.setOverrideCursor(Qt.WaitCursor) logging.debug("Send money...") - result = await self.model.send_money(recipient, amount, comment, password) + result, transaction = await self.model.send_money(recipient, password, amount, amount_base, comment) if result[0]: - await self.view.show_success(self.model.app.preferences['notifications'], recipient) + await self.view.show_success(self.model.notifications(), recipient) logging.debug("Restore cursor...") QApplication.restoreOverrideCursor() # If we sent back a transaction we cancel the first one self.model.cancel_previous() - self.model.app.refresh_transfers.emit() + self.model.app.new_transfer.emit(transaction) self.view.accept() else: - await self.view.show_error(self.model.app.preferences['notifications'], result[1]) + await self.view.show_error(self.model.notifications(), result[1]) QApplication.restoreOverrideCursor() self.view.button_box.setEnabled(True) @@ -196,40 +182,48 @@ class TransferController(QObject): def reject(self): self.view.reject() - @asyncify - async def refresh(self): - amount = await self.model.wallet_value() - total_text = await self.model.localized_amount(amount) - self.view.refresh_labels(total_text, self.model.community.currency) + def refresh(self): + amount = self.model.wallet_value() + total_text = self.model.localized_amount(amount) + self.view.refresh_labels(total_text) if amount == 0: self.view.set_button_box(TransferView.ButtonBoxState.NO_AMOUNT) else: self.view.set_button_box(TransferView.ButtonBoxState.OK) - max_relative = await self.model.quant_to_rel(amount) - current_base = await self.model.current_base() + max_relative = self.model.quant_to_rel(amount) + current_base = self.model.current_base() self.view.set_spinboxes_parameters(pow(10, current_base), amount, max_relative) - @asyncify - async def handle_amount_change(self, value): - relative = await self.model.quant_to_rel(value) + def handle_amount_change(self, value): + relative = self.model.quant_to_rel(value) self.view.change_relative_amount(relative) - - @asyncify - async def handle_relative_change(self, value): - amount = await self.model.rel_to_quant(value) + self.refresh_amount_suffix() + + def refresh_amount_suffix(self): + #TODO: Handle other exponents than 0 (using a custom spinbox ?) + unicodes = { + '0': ord('\u2070'), + '1': ord('\u00B9'), + '2': ord('\u00B2'), + '3': ord('\u00B3'), + } + for n in range(4, 10): + unicodes[str(n)] = ord('\u2070') + n + + exponent = "" + for n in str('0'): + exponent += chr(unicodes[n]) + self.view.spinbox_amount.setSuffix(" x10" + exponent + " " + self.model.connection.currency) + + def handle_relative_change(self, value): + amount = self.model.rel_to_quant(value) self.view.change_quantitative_amount(amount) - def change_current_community(self, index): - self.model.change_community(index) - self.search_user.set_community(self.community) - self.user_information.change_community(self.community) - self.refresh() - - def change_current_wallet(self, index): - self.model.change_wallet(index) + def change_current_connection(self, index): + self.model.set_connection(index) self.refresh() def async_exec(self): diff --git a/src/sakia/gui/dialogs/transfer/model.py b/src/sakia/gui/dialogs/transfer/model.py index ae51a9a0..91c8072b 100644 --- a/src/sakia/gui/dialogs/transfer/model.py +++ b/src/sakia/gui/dialogs/transfer/model.py @@ -1,29 +1,30 @@ +import attr from PyQt5.QtCore import QObject +from sakia.data.processors import BlockchainProcessor, SourcesProcessor, ConnectionsProcessor +@attr.s() class TransferModel(QObject): """ The model of transfer component + + :param sakia.app.Application app: + :param sakia.data.entities.Connection connection: + :param sakia.data.processors.BlockchainProcessor _blockchain_processor: """ - def __init__(self, parent, app, account, community, resent_transfer): - super().__init__(parent) - self.app = app - self.account = account - self.resent_transfer = resent_transfer - self.community = community if community else self.account.communities[0] - self.wallet = self.account.wallets[0] + app = attr.ib() + connection = attr.ib(default=None) + resent_transfer = attr.ib(default=None) + _blockchain_processor = attr.ib(default=None) + _sources_processor = attr.ib(default=None) + _connections_processor = attr.ib(default=None) - def contact_name_pubkey(self, name): - """ - Get the pubkey of a contact from its name - :param str name: - :return: - :rtype: str - """ - for contact in self.account.contacts: - if contact['name'] == name: - return contact['pubkey'] + def __attrs_post_init__(self): + super().__init__() + self._blockchain_processor = BlockchainProcessor.instanciate(self.app) + self._sources_processor = SourcesProcessor.instanciate(self.app) + self._connections_processor = ConnectionsProcessor.instanciate(self.app) async def rel_to_quant(self, rel_value): """ @@ -31,88 +32,69 @@ class TransferModel(QObject): :param float rel_value: :rtype: int """ - ud_block = await self.community.get_ud_block() - if ud_block: - dividend = ud_block['dividend'] - base = ud_block['unitbase'] - else: - dividend = 1 - base = 0 + dividend, base = await self._blockchain_processor.last_ud(self.connection.currency) amount = rel_value * dividend * pow(10, base) # amount is rounded to the nearest power of 10 depending of last ud base rounded = int(pow(10, base) * round(float(amount) / pow(10, base))) return rounded - async def quant_to_rel(self, amount): + def quant_to_rel(self, amount): """ Get the relative value of a given amount :param int amount: :rtype: float """ - - ud_block = await self.community.get_ud_block() - if ud_block: - dividend = ud_block['dividend'] - base = ud_block['unitbase'] - else: - dividend = 1 - base = 0 + dividend, base = self._blockchain_processor.last_ud(self.connection.currency) relative = amount / (dividend * pow(10, base)) return relative - async def wallet_value(self): + def wallet_value(self): """ Get the value of the current wallet in the current community """ - return await self.wallet.value(self.community) + return self._sources_processor.amount(self.connection.currency, self.connection.pubkey) - async def current_base(self): + def current_base(self): """ Get the current base of the network """ - ud_block = await self.community.get_ud_block() - if ud_block: - base = ud_block['unitbase'] - else: - base = 0 + dividend, base = self._blockchain_processor.last_ud(self.connection.currency) return base - async def localized_amount(self, amount): + def localized_amount(self, amount): """ Get the value of the current referential """ - localized = await self.account.current_ref.instance(amount, self.community, self.app) \ + + localized = self.app.current_ref.instance(amount, self.connection.currency, self.app) \ .diff_localized(units=True, - international_system=self.app.preferences['international_system_of_units']) + international_system=self.app.parameters.international_system_of_units) return localized - def change_community(self, index): - """ - Change the current community - :param int index: index in the list of communities - """ - self.community = self.account.communities[index] - - def change_wallet(self, index): - """ - Change the current wallet - :param int index: index in the list of wallets - """ - self.wallet = self.account.wallets[index] - def cancel_previous(self): if self.resent_transfer: self.resent_transfer.cancel() - async def send_money(self, recipient, amount, comment, password): + def available_connections(self): + return self._connections_processor.connections() + + def set_connection(self, index): + connections = self._connections_processor.connections() + self.connection = connections[index] + + async def send_money(self, recipient, password, amount, amount_base, comment): """ Send money to given recipient using the account :param str recipient: :param int amount: + :param int amount_base: :param str comment: :param str password: :return: the result of the send """ - return await self.wallet.send_money(self.account.salt, password, self.community, - recipient, amount, comment) \ No newline at end of file + return await self.app.documents_service.send_money(self.connection, password, + recipient, amount, amount_base, comment) + + def notifications(self): + return self.app.parameters.notifications diff --git a/src/sakia/gui/dialogs/transfer/transfer.py b/src/sakia/gui/dialogs/transfer/transfer.py index 39bf940c..343b93f3 100644 --- a/src/sakia/gui/dialogs/transfer/transfer.py +++ b/src/sakia/gui/dialogs/transfer/transfer.py @@ -61,7 +61,7 @@ class TransferMoneyDialog(QObject): self.ui.search_user.button_reset.hide() self.ui.search_user.init(self.app) self.ui.search_user.change_account(self.account) - self.ui.search_user.change_community(self.community) + self.ui.search_user.change_currency(self.community) self.ui.search_user.search_started.connect(lambda: self.ui.button_box.setEnabled(False)) self.ui.search_user.search_completed.connect(lambda: self.ui.button_box.setEnabled(True)) diff --git a/src/sakia/gui/dialogs/transfer/transfer.ui b/src/sakia/gui/dialogs/transfer/transfer.ui index 5c5f691f..e8f16f25 100644 --- a/src/sakia/gui/dialogs/transfer/transfer.ui +++ b/src/sakia/gui/dialogs/transfer/transfer.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>800</width> - <height>500</height> + <width>566</width> + <height>415</height> </rect> </property> <property name="windowTitle"> @@ -17,11 +17,11 @@ <item> <widget class="QGroupBox" name="groupBox_2"> <property name="title"> - <string>Community</string> + <string>Select connection</string> </property> <layout class="QHBoxLayout" name="horizontalLayout_4"> <item> - <widget class="QComboBox" name="combo_community"/> + <widget class="QComboBox" name="combo_connections"/> </item> </layout> </widget> @@ -40,55 +40,6 @@ <property name="bottomMargin"> <number>6</number> </property> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_2"> - <item> - <widget class="QRadioButton" name="radio_contact"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string>Con&tact</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_3"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Maximum</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QComboBox" name="combo_contact"> - <property name="enabled"> - <bool>true</bool> - </property> - <property name="sizePolicy"> - <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - </widget> - </item> - </layout> - </item> <item> <layout class="QHBoxLayout" name="horizontalLayout"> <item> @@ -197,30 +148,6 @@ <enum>QFrame::Raised</enum> </property> <layout class="QVBoxLayout" name="verticalLayout_8"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_7"> - <property name="topMargin"> - <number>5</number> - </property> - <item> - <widget class="QLabel" name="label_2"> - <property name="text"> - <string>Wallet</string> - </property> - </widget> - </item> - <item> - <widget class="QComboBox" name="combo_wallets"> - <property name="minimumSize"> - <size> - <width>0</width> - <height>30</height> - </size> - </property> - </widget> - </item> - </layout> - </item> <item> <widget class="QLabel" name="label_total"> <property name="text"> diff --git a/src/sakia/gui/dialogs/transfer/view.py b/src/sakia/gui/dialogs/transfer/view.py index c44be0ea..67b448d0 100644 --- a/src/sakia/gui/dialogs/transfer/view.py +++ b/src/sakia/gui/dialogs/transfer/view.py @@ -17,7 +17,6 @@ class TransferView(QDialog, Ui_TransferMoneyDialog): OK = 1 class RecipientMode(Enum): - CONTACT = 0 PUBKEY = 1 SEARCH = 2 @@ -27,21 +26,16 @@ class TransferView(QDialog, Ui_TransferMoneyDialog): ButtonBoxState.OK: (True, QT_TRANSLATE_NOOP("CertificationView", "&Ok")) } - def __init__(self, parent, search_user_view, user_information_view, - communities_names, contacts_names, wallets_names): + def __init__(self, parent, search_user_view, user_information_view): """ :param parent: :param sakia.gui.search_user.view.SearchUserView search_user_view: :param sakia.gui.user_information.view.UserInformationView user_information_view: - :param list[str] communities_names: - :param list[str] contacts_names: - :param list[str] wallets_names: """ super().__init__(parent) self.setupUi(self) - self.radio_contact.toggled.connect(lambda c, radio=TransferView.RecipientMode.CONTACT: self.recipient_mode_changed(radio)) self.radio_pubkey.toggled.connect(lambda c, radio=TransferView.RecipientMode.PUBKEY: self.recipient_mode_changed(radio)) self.radio_search.toggled.connect(lambda c, radio=TransferView.RecipientMode.SEARCH: self.recipient_mode_changed(radio)) @@ -49,33 +43,20 @@ class TransferView(QDialog, Ui_TransferMoneyDialog): validator = QRegExpValidator(regexp) self.edit_message.setValidator(validator) - for name in communities_names: - self.combo_community.addItem(name) - - for name in sorted(contacts_names): - self.combo_contact.addItem(name) - - for name in wallets_names: - self.combo_wallets.addItem(name) - - if len(contacts_names) == 0: - self.combo_contact.setEnabled(False) - self.radio_pubkey.setChecked(True) - self.radio_contact.setEnabled(False) - self.search_user = search_user_view self.user_information_view = user_information_view + self._amount_base = 0 + self._currency = "" def recipient_mode(self): - if self.radio_contact.isChecked(): - return TransferView.RecipientMode.CONTACT - elif self.radio_search.isChecked(): + if self.radio_search.isChecked(): return TransferView.RecipientMode.SEARCH else: return TransferView.RecipientMode.PUBKEY - def selected_contact(self): - return self.combo_contact.currentText() + def set_keys(self, connections): + for conn in connections: + self.combo_connections.addItem(conn.title()) def set_search_user(self, search_user_view): """ @@ -96,7 +77,6 @@ class TransferView(QDialog, Ui_TransferMoneyDialog): :param str radio: """ self.edit_pubkey.setEnabled(radio == TransferView.RecipientMode.PUBKEY) - self.combo_contact.setEnabled(radio == TransferView.RecipientMode.CONTACT) self.search_user.setEnabled(radio == TransferView.RecipientMode.SEARCH) def change_quantitative_amount(self, amount): @@ -129,14 +109,13 @@ class TransferView(QDialog, Ui_TransferMoneyDialog): self.spinbox_relative.setMaximum(max_rel) self.spinbox_amount.setSingleStep(tick_quant) - def refresh_labels(self, total_text, currency): + def refresh_labels(self, total_text): """ Refresh displayed texts :param str total_text: :param str currency: """ self.label_total.setText("{0}".format(total_text)) - self.spinbox_amount.setSuffix(" " + currency) def set_button_box(self, state, **kwargs): """ @@ -162,3 +141,6 @@ class TransferView(QDialog, Ui_TransferMoneyDialog): toast.display(self.tr("Transfer"), "Error : {0}".format(error_txt)) else: await QAsyncMessageBox.critical(self.widget, self.tr("Transfer"), error_txt) + + def pubkey_value(self): + return self.edit_pubkey.text() \ No newline at end of file diff --git a/src/sakia/gui/sub/user_information/model.py b/src/sakia/gui/sub/user_information/model.py index 3b8e62ef..11c74185 100644 --- a/src/sakia/gui/sub/user_information/model.py +++ b/src/sakia/gui/sub/user_information/model.py @@ -24,7 +24,10 @@ class UserInformationModel(QObject): if identity: self.certs_sent = self._certifications_processor.certifications_sent(currency, identity.pubkey) self.certs_received = self._certifications_processor.certifications_received(currency, identity.pubkey) - self.identities_service = self.app.identities_services[self.currency] + if currency: + self.identities_service = self.app.identities_services[self.currency] + else: + self.identities_service = None async def load_identity(self, identity): """ diff --git a/src/sakia/services/documents.py b/src/sakia/services/documents.py index 85453e40..5edb8fa4 100644 --- a/src/sakia/services/documents.py +++ b/src/sakia/services/documents.py @@ -6,13 +6,18 @@ from collections import Counter from duniterpy.key import SigningKey from duniterpy import PROTOCOL_VERSION -from duniterpy.documents import BlockUID, Block, Certification, Membership, Revocation +from duniterpy.documents import BlockUID, Block, Certification, Membership, Revocation, InputSource, \ + OutputSource, SIGParameter, Unlock from duniterpy.documents import Identity as IdentityDoc +from duniterpy.documents import Transaction as TransactionDoc +from duniterpy.documents.transaction import reduce_base +from duniterpy.grammars import output from duniterpy.api import bma, errors -from sakia.data.entities import Identity -from sakia.data.processors import BlockchainProcessor, IdentitiesProcessor, NodesProcessor +from sakia.data.entities import Identity, Transaction +from sakia.data.processors import BlockchainProcessor, IdentitiesProcessor, NodesProcessor, \ + TransactionsProcessor, SourcesProcessor from sakia.data.connectors import BmaConnector -from aiohttp.errors import ClientError, DisconnectedError +from sakia.errors import NotEnoughChangeError @attr.s() @@ -20,10 +25,18 @@ class DocumentsService: """ A service to forge and broadcast documents to the network + + :param sakia.data.connectors.BmaConnector _bma_connector: the connector + :param sakia.data.processors.BlockchainProcessor _blockchain_processor: the blockchain processor + :param sakia.data.processors.IdentitiesProcessor _identities_processor: the identities processor + :param sakia.data.processors.TransactionsProcessor _transactions_processor: the transactions processor + :param sakia.data.processors.SourcesProcessor _sources_processor: the sources processor """ - _bma_connector = attr.ib() # :type: sakia.data.connectors.BmaConnector - _blockchain_processor = attr.ib() # :type: sakia.data.processors.BlockchainProcessor - _identities_processor = attr.ib() # :type: sakia.data.processors.IdentitiesProcessor + _bma_connector = attr.ib() + _blockchain_processor = attr.ib() + _identities_processor = attr.ib() + _transactions_processor = attr.ib() + _sources_processor = attr.ib() _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger('sakia'))) @classmethod @@ -34,7 +47,9 @@ class DocumentsService: """ return cls(BmaConnector(NodesProcessor(app.db.nodes_repo)), BlockchainProcessor.instanciate(app), - IdentitiesProcessor.instanciate(app)) + IdentitiesProcessor.instanciate(app), + TransactionsProcessor.instanciate(app), + SourcesProcessor.instanciate(app)) async def broadcast_identity(self, connection, password): """ @@ -73,6 +88,8 @@ class DocumentsService: identity.blockstamp = block_uid identity.signature = selfcert.signatures[0] identity.timestamp = timestamp + else: + identity = None return result, identity @@ -189,3 +206,181 @@ class DocumentsService: document.sign(self_cert, [key]) return document.signed_raw(self_cert) + + def tx_sources(self, amount, amount_base, currency): + """ + Get inputs to generate a transaction with a given amount of money + :param int amount: The amount target value + :param int amount_base: The amount base target value + :param str currency: The community target of the transaction + :return: The list of inputs to use in the transaction document + """ + + # such a dirty algorithmm + # everything should be done again from scratch + # in future versions + + def current_value(inputs, overhs): + i = 0 + for s in inputs: + i += s.amount * (10**s.base) + for o in overhs: + i -= o[0] * (10**o[1]) + return i + + amount, amount_base = reduce_base(amount, amount_base) + available_sources = self._sources_processor.available(currency) + if available_sources: + current_base = max([src.base for src in available_sources]) + value = 0 + sources = [] + outputs = [] + overheads = [] + buf_sources = list(available_sources) + while current_base >= 0: + for s in [src for src in available_sources if src.base == current_base]: + test_sources = sources + [s] + val = current_value(test_sources, overheads) + # if we have to compute an overhead + if current_value(test_sources, overheads) > amount * (10**amount_base): + overhead = current_value(test_sources, overheads) - int(amount) * (10**amount_base) + # we round the overhead in the current base + # exemple : 12 in base 1 -> 1*10^1 + overhead = int(round(float(overhead) / (10**current_base))) + source_value = s.amount * (10**s.base) + out = int((source_value - (overhead * (10**current_base)))/(10**current_base)) + if out * (10**current_base) <= amount * (10**amount_base): + sources.append(s) + buf_sources.remove(s) + overheads.append((overhead, current_base)) + outputs.append((out, current_base)) + # else just add the output + else: + sources.append(s) + buf_sources.remove(s) + outputs.append((s.amount, s.base)) + if current_value(sources, overheads) == amount * (10 ** amount_base): + return sources, outputs, overheads + + current_base -= 1 + + raise NotEnoughChangeError(value, currency, len(sources), amount * pow(10, amount_base)) + + def tx_inputs(self, sources): + """ + Get inputs to generate a transaction with a given amount of money + :param list[sakia.data.entities.Source] sources: The sources used to send the given amount of money + :return: The list of inputs to use in the transaction document + """ + inputs = [] + for s in sources: + inputs.append(InputSource(s.amount, s.base, s.type, s.identifier, s.noffset)) + return inputs + + def tx_unlocks(self, sources): + """ + Get unlocks to generate a transaction with a given amount of money + :param list sources: The sources used to send the given amount of money + :return: The list of unlocks to use in the transaction document + """ + unlocks = [] + for i, s in enumerate(sources): + unlocks.append(Unlock(i, [SIGParameter(0)])) + return unlocks + + def tx_outputs(self, issuer, receiver, outputs, overheads): + """ + Get outputs to generate a transaction with a given amount of money + :param str issuer: The issuer of the transaction + :param str receiver: The target of the transaction + :param list outputs: The amount to send + :param list inputs: The inputs used to send the given amount of money + :param list overheads: The overheads used to send the given amount of money + :return: The list of outputs to use in the transaction document + """ + total = [] + outputs_bases = set(o[1] for o in outputs) + for base in outputs_bases: + output_sum = 0 + for o in outputs: + if o[1] == base: + output_sum += o[0] + total.append(OutputSource(output_sum, base, output.Condition.token(output.SIG.token(receiver)))) + + overheads_bases = set(o[1] for o in overheads) + for base in overheads_bases: + overheads_sum = 0 + for o in overheads: + if o[1] == base: + overheads_sum += o[0] + total.append(OutputSource(overheads_sum, base, output.Condition.token(output.SIG.token(issuer)))) + + return total + + def prepare_tx(self, issuer, receiver, blockstamp, amount, amount_base, message, currency): + """ + Prepare a simple Transaction document + :param str issuer: the issuer of the transaction + :param str receiver: the target of the transaction + :param duniterpy.documents.BlockUID blockstamp: the blockstamp + :param int amount: the amount sent to the receiver + :param int amount_base: the amount base of the currency + :param str message: the comment of the tx + :param str currency: the target community + :return: the transaction document + :rtype: duniterpy.documents.Transaction + """ + result = self.tx_sources(int(amount), amount_base, currency) + sources = result[0] + computed_outputs = result[1] + overheads = result[2] + self._sources_processor.consume(sources) + logging.debug("Inputs : {0}".format(sources)) + + inputs = self.tx_inputs(sources) + unlocks = self.tx_unlocks(sources) + outputs = self.tx_outputs(issuer, receiver, computed_outputs, overheads) + logging.debug("Outputs : {0}".format(outputs)) + tx = TransactionDoc(3, currency, blockstamp, 0, + [issuer], inputs, unlocks, + outputs, message, None) + return tx + + async def send_money(self, connection, password, recipient, amount, amount_base, message): + """ + Send money to a given recipient in a specified community + :param sakia.data.entities.Connection connection: The account salt + :param str password: The account password + :param str recipient: The pubkey of the recipient + :param int amount: The amount of money to transfer + :param int amount_base: The amount base of the transfer + :param str message: The message to send with the transfer + """ + blockstamp = self._blockchain_processor.current_buid(connection.currency) + time = self._blockchain_processor.time(connection.currency) + key = SigningKey(connection.salt, password, connection.scrypt_params) + logging.debug("Sender pubkey:{0}".format(key.pubkey)) + try: + txdoc = self.prepare_tx(connection.pubkey, recipient, blockstamp, amount, amount_base, + message, connection.currency) + logging.debug("TX : {0}".format(txdoc.raw())) + + txdoc.sign([key]) + logging.debug("Transaction : [{0}]".format(txdoc.signed_raw())) + txid = self._transactions_processor.next_txid(connection.currency, blockstamp.number) + tx = Transaction(currency=connection.currency, + sha_hash=txdoc.sha_hash, + written_block=0, + blockstamp=blockstamp, + timestamp=time, + signature=txdoc.signatures[0], + issuer=connection.pubkey, + receiver=recipient, + amount=amount, + amount_base=amount_base, + comment=message, + txid=txid, + state=Transaction.TO_SEND) + return await self._transactions_processor.send(tx, txdoc, connection.currency) + except NotEnoughChangeError as e: + return (False, str(e)), None diff --git a/src/sakia/tests/conftest.py b/src/sakia/tests/conftest.py index bab6a63b..07cd5f33 100644 --- a/src/sakia/tests/conftest.py +++ b/src/sakia/tests/conftest.py @@ -146,6 +146,14 @@ def application_with_one_connection(application, simple_fake_server, bob): previous_ud_time=previous_ud_block.mediantime, currency=simple_fake_server.forge.currency) application.db.blockchains_repo.insert(blockchain) + for s in simple_fake_server.forge.user_identities[bob.key.pubkey].sources: + application.db.sources_repo.insert(Source(currency=simple_fake_server.forge.currency, + pubkey=bob.key.pubkey, + identifier=s.origin_id, + noffset=s.index, + type=s.source, + amount=s.amount, + base=s.base)) bob_blockstamp = simple_fake_server.forge.user_identities[bob.key.pubkey].blockstamp bob_user_identity = simple_fake_server.forge.user_identities[bob.key.pubkey] bob_ms = bob_user_identity.memberships[-1] @@ -155,8 +163,8 @@ def application_with_one_connection(application, simple_fake_server, bob): blockstamp=bob_blockstamp, signature=bob_user_identity.signature, timestamp=simple_fake_server.forge.blocks[bob_blockstamp.number].mediantime, - written_on=None, - revoked_on=bob_user_identity.revoked_on, + written_on=0, + revoked_on=0, member=bob_user_identity.member, membership_buid=bob_ms.blockstamp, membership_timestamp=simple_fake_server.forge.blocks[bob_ms.blockstamp.number].mediantime, diff --git a/src/sakia/tests/functional/identities_tab/test_identities_table.py b/src/sakia/tests/functional/identities_tab/test_identities_table.py index d64481fa..8342015c 100644 --- a/src/sakia/tests/functional/identities_tab/test_identities_table.py +++ b/src/sakia/tests/functional/identities_tab/test_identities_table.py @@ -69,7 +69,7 @@ class TestIdentitiesTable(unittest.TestCase, QuamashTest): self.addCleanup(srv.close) identities_tab.change_account(self.account, self.password_asker) - identities_tab.change_community(self.community) + identities_tab.change_currency(self.community) await asyncio.sleep(1) QTest.keyClicks(identities_tab.ui.edit_textsearch, "doe") diff --git a/src/sakia/tests/functional/test_certification_dialog.py b/src/sakia/tests/functional/test_certification_dialog.py index 6e5f7503..c96668e9 100644 --- a/src/sakia/tests/functional/test_certification_dialog.py +++ b/src/sakia/tests/functional/test_certification_dialog.py @@ -35,7 +35,7 @@ async def test_certification_init_community(application_with_one_connection, fak assert certification_dialog.view.button_box.button(QDialogButtonBox.Ok).isEnabled() QTest.mouseClick(certification_dialog.view.button_box.button(QDialogButtonBox.Ok), Qt.LeftButton) await asyncio.sleep(0.1) - assert Certification is type(fake_server.forge.pool[0]) + assert isinstance(fake_server.forge.pool[0], Certification) application_with_one_connection.loop.call_later(10, close_dialog) asyncio.ensure_future(exec_test()) diff --git a/src/sakia/tests/functional/test_transfer_dialog.py b/src/sakia/tests/functional/test_transfer_dialog.py new file mode 100644 index 00000000..74a63db9 --- /dev/null +++ b/src/sakia/tests/functional/test_transfer_dialog.py @@ -0,0 +1,32 @@ +import asyncio +import pytest +from PyQt5.QtCore import QLocale, Qt +from PyQt5.QtTest import QTest +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QMessageBox, QApplication +from sakia.gui.dialogs.transfer.controller import TransferController +from duniterpy.documents import Transaction + + +@pytest.mark.asyncio +async def test_transfer(application_with_one_connection, simple_fake_server, bob, alice): + transfer_dialog = TransferController.create(None, application_with_one_connection) + + def close_dialog(): + if transfer_dialog.view.isVisible(): + transfer_dialog.view.close() + + async def exec_test(): + transfer_dialog.model.connection.password = bob.password + QTest.mouseClick(transfer_dialog.view.radio_pubkey, Qt.LeftButton) + QTest.keyClicks(transfer_dialog.view.edit_pubkey, alice.key.pubkey) + transfer_dialog.view.spinbox_amount.setValue(10) + await asyncio.sleep(0.1) + assert transfer_dialog.view.button_box.button(QDialogButtonBox.Ok).isEnabled() + QTest.mouseClick(transfer_dialog.view.button_box.button(QDialogButtonBox.Ok), Qt.LeftButton) + await asyncio.sleep(0.1) + assert isinstance(simple_fake_server.forge.pool[0], Transaction) + + application_with_one_connection.loop.call_later(10, close_dialog) + asyncio.ensure_future(exec_test()) + await transfer_dialog.async_exec() + await simple_fake_server.close() diff --git a/src/sakia/tests/functional/transfer/test_transfer.py b/src/sakia/tests/functional/transfer/test_transfer.py deleted file mode 100644 index 087bb872..00000000 --- a/src/sakia/tests/functional/transfer/test_transfer.py +++ /dev/null @@ -1,89 +0,0 @@ -import asyncio -import time -import unittest - -import aiohttp -from PyQt5.QtCore import QLocale, Qt -from PyQt5.QtTest import QTest -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QMessageBox, QApplication -from sakia.core.net import Network, Node -from sakia.core.net.api.bma.access import BmaAccess -from sakia.gui.transfer import TransferMoneyDialog - -from sakia.app import Application -from sakia.core import Account, Community, Wallet -from sakia.core.registry.identities import IdentitiesRegistry -from sakia.gui.password_asker import PasswordAskerDialog -from sakia.tests import QuamashTest -from sakia.tests.mocks.bma import nice_blockchain - - -class TestTransferDialog(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - self.identities_registry = IdentitiesRegistry({}) - - self.application = Application(self.qapplication, self.lp, self.identities_registry) - self.application.preferences['notifications'] = False - - self.mock_nice_blockchain = nice_blockchain.get_mock(self.lp) - self.node = Node(self.mock_nice_blockchain.peer(), - "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", - None, Node.ONLINE, - time.time(), {}, "duniter", "0.14.0", 0, session=aiohttp.ClientSession()) - self.network = Network.create(self.node) - self.bma_access = BmaAccess.create(self.network) - self.community = Community("test_currency", self.network, self.bma_access) - - self.wallet = Wallet(0, "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "Wallet 1", self.identities_registry) - - # Salt/password : "testsakia/testsakia" - # Pubkey : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ - self.account = Account("testsakia", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "john", [self.community], [self.wallet], [], self.identities_registry) - - self.password_asker = PasswordAskerDialog(self.account) - self.password_asker.password = "testsakia" - self.password_asker.remember = True - - def tearDown(self): - self.tearDownQuamash() - - def test_transfer_nice_community(self): - transfer_dialog = TransferMoneyDialog(self.application, - self.account, - self.password_asker, - self.community, - None) - self.account.wallets[0].init_cache(self.application, self.community) - - async def open_dialog(transfer_dialog): - srv, port, url = await self.mock_nice_blockchain.create_server() - self.addCleanup(srv.close) - await asyncio.sleep(1) - result = await transfer_dialog.async_exec() - await self.mock_nice_blockchain.close() - self.assertEqual(result, QDialog.Accepted) - - def close_dialog(): - if transfer_dialog.widget.isVisible(): - transfer_dialog.widget.close() - - async def exec_test(): - self.account.wallets[0].caches[self.community.currency].available_sources = await self.wallet.sources(self.community) - QTest.mouseClick(transfer_dialog.ui.radio_pubkey, Qt.LeftButton) - QTest.keyClicks(transfer_dialog.ui.edit_pubkey, "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn") - transfer_dialog.ui.spinbox_amount.setValue(10) - QTest.mouseClick(transfer_dialog.ui.button_box.button(QDialogButtonBox.Ok), Qt.LeftButton) - await asyncio.sleep(1) - topWidgets = QApplication.topLevelWidgets() - for w in topWidgets: - if type(w) is QMessageBox: - QTest.keyClick(w, Qt.Key_Enter) - await asyncio.sleep(1) - - self.lp.call_later(30, close_dialog) - asyncio.ensure_future(exec_test()) - self.lp.run_until_complete(open_dialog(transfer_dialog)) -- GitLab