diff --git a/src/sakia/data/processors/transactions.py b/src/sakia/data/processors/transactions.py index 2ed0d93a9d2357e48ec6c46e9fe82c61ddf3c46d..01421663e114c4a69588d7109d3cc5b7620a4f99 100644 --- a/src/sakia/data/processors/transactions.py +++ b/src/sakia/data/processors/transactions.py @@ -110,19 +110,17 @@ class TransactionsProcessor: """ return self.run_state_transitions(tx) - async def send(self, tx, txdoc, currency): + async def send(self, tx, currency): """ Send a transaction and update the transfer state to AWAITING if accepted. If the transaction was refused (return code != 200), state becomes REFUSED The txdoc is saved as the transfer txdoc. :param sakia.data.entities.Transaction tx: the transaction - :param txdoc: A transaction duniterpy object :param currency: The community target of the transaction """ - self._logger.debug(txdoc.signed_raw()) self._repo.insert(tx) - responses = await self._bma_connector.broadcast(currency, bma.tx.process, req_args={'transaction': txdoc.signed_raw()}) + responses = await self._bma_connector.broadcast(currency, bma.tx.process, req_args={'transaction': tx.raw}) result = await parse_bma_responses(responses) self.run_state_transitions(tx, [r.status for r in responses if not isinstance(r, BaseException)]) return result, tx diff --git a/src/sakia/gui/dialogs/transfer/controller.py b/src/sakia/gui/dialogs/transfer/controller.py index 3e46aa3975a156b9bcd86bc9b6c54a5af1736a87..18b6f17b18294a590b1bf1113429225b061a21c2 100644 --- a/src/sakia/gui/dialogs/transfer/controller.py +++ b/src/sakia/gui/dialogs/transfer/controller.py @@ -146,7 +146,7 @@ class TransferController(QObject): QApplication.setOverrideCursor(Qt.WaitCursor) logging.debug("Send money...") - result, transaction = await self.model.send_money(recipient, secret_key, password, amount, amount_base, comment) + result, transactions = await self.model.send_money(recipient, secret_key, password, amount, amount_base, comment) if result[0]: await self.view.show_success(self.model.notifications(), recipient) logging.debug("Restore cursor...") @@ -154,11 +154,13 @@ class TransferController(QObject): # If we sent back a transaction we cancel the first one self.model.cancel_previous() - self.model.app.new_transfer.emit(transaction) + for tx in transactions: + self.model.app.new_transfer.emit(tx) self.view.accept() else: await self.view.show_error(self.model.notifications(), result[1]) - self.model.app.new_transfer.emit(transaction) + for tx in transactions: + self.model.app.new_transfer.emit(tx) QApplication.restoreOverrideCursor() self.view.button_box.setEnabled(True) diff --git a/src/sakia/gui/dialogs/transfer/model.py b/src/sakia/gui/dialogs/transfer/model.py index b873bfdc17163d2ca5cff957fe9fb5f4be1c85b2..90e015265f4d0f27b01bd819a51fd4d938d73acd 100644 --- a/src/sakia/gui/dialogs/transfer/model.py +++ b/src/sakia/gui/dialogs/transfer/model.py @@ -90,14 +90,15 @@ class TransferModel(QObject): :return: the result of the send """ - result, transaction = await self.app.documents_service.send_money(self.connection, secret_key, password, + result, transactions = await self.app.documents_service.send_money(self.connection, secret_key, password, recipient, amount, amount_base, comment) - self.app.sources_service.parse_transaction(self.connection.pubkey, transaction) - if recipient in self._connections_processor.pubkeys(): - self.app.sources_service.parse_transaction(recipient, transaction) - self.app.db.commit() - self.app.sources_refreshed.emit() - return result, transaction + for transaction in transactions: + self.app.sources_service.parse_transaction(self.connection.pubkey, transaction) + if recipient in self._connections_processor.pubkeys(): + self.app.sources_service.parse_transaction(recipient, transaction) + self.app.db.commit() + self.app.sources_refreshed.emit() + return result, transactions def notifications(self): return self.app.parameters.notifications diff --git a/src/sakia/services/documents.py b/src/sakia/services/documents.py index e7c72902ddd7bf28f78c59897262a1bb886a25c2..fda588eb8b6d8d29cbfd5e3752ae4d5a95c0fb4d 100644 --- a/src/sakia/services/documents.py +++ b/src/sakia/services/documents.py @@ -10,7 +10,7 @@ from duniterpy.documents import Transaction as TransactionDoc from duniterpy.documents.transaction import reduce_base from duniterpy.grammars import output from duniterpy.api import bma -from sakia.data.entities import Identity, Transaction +from sakia.data.entities import Identity, Transaction, Source from sakia.data.processors import BlockchainProcessor, IdentitiesProcessor, NodesProcessor, \ TransactionsProcessor, SourcesProcessor, CertificationsProcessor from sakia.data.connectors import BmaConnector, parse_bma_responses @@ -326,10 +326,29 @@ class DocumentsService: return total - def prepare_tx(self, issuer, receiver, blockstamp, amount, amount_base, message, currency): + def commit_outputs_to_self(self, currency, pubkey, txdoc): + """ + Save outputs to self + :param str currency: + :param str pubkey: + :param TransactionDoc txdoc: + :return: + """ + for offset, output in enumerate(txdoc.outputs): + if output.conditions.left.pubkey == pubkey: + source = Source(currency=currency, + pubkey=pubkey, + identifier=txdoc.sha_hash, + type='T', + noffset=offset, + amount=output.amount, + base=output.base) + self._sources_processor.insert(source) + + def prepare_tx(self, key, receiver, blockstamp, amount, amount_base, message, currency): """ Prepare a simple Transaction document - :param str issuer: the issuer of the transaction + :param SigningKey key: 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 @@ -337,23 +356,54 @@ class DocumentsService: :param str message: the comment of the tx :param str currency: the target community :return: the transaction document - :rtype: duniterpy.documents.Transaction + :rtype: List[sakia.data.entities.Transaction] """ - result = self.tx_sources(int(amount), amount_base, currency, issuer) - sources = result[0] - computed_outputs = result[1] - overheads = result[2] + forged_tx = [] + sources = [None]*41 + while len(sources) > 40: + result = self.tx_sources(int(amount), amount_base, currency, key.pubkey) + 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 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) + outputs = self.tx_outputs(key.pubkey, receiver, computed_outputs, overheads) logging.debug("Outputs : {0}".format(outputs)) - tx = TransactionDoc(10, currency, blockstamp, 0, - [issuer], inputs, unlocks, - outputs, message, None) - return tx + txdoc = TransactionDoc(10, currency, blockstamp, 0, + [key.pubkey], inputs, unlocks, + outputs, message, None) + txdoc.sign([key]) + self.commit_outputs_to_self(currency, key.pubkey, txdoc) + time = self._blockchain_processor.time(currency) + tx = Transaction(currency=currency, + sha_hash=txdoc.sha_hash, + written_block=0, + blockstamp=blockstamp, + timestamp=time, + signature=txdoc.signatures[0], + issuer=key.pubkey, + receiver=receiver, + amount=amount, + amount_base=amount_base, + comment=txdoc.comment, + txid=0, + state=Transaction.TO_SEND, + local=True, + raw=txdoc.signed_raw()) + forged_tx.append(tx) + return forged_tx async def send_money(self, connection, secret_key, password, recipient, amount, amount_base, message): """ @@ -367,32 +417,25 @@ class DocumentsService: :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(secret_key, password, connection.scrypt_params) logging.debug("Sender pubkey:{0}".format(key.pubkey)) + tx_entities = [] + result = (True, ""), tx_entities 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, - local=True, - raw=txdoc.signed_raw()) - return await self._transactions_processor.send(tx, txdoc, connection.currency) + tx_entities = self.prepare_tx(key, recipient, blockstamp, amount, amount_base, + message, connection.currency) + + for i, tx in enumerate(tx_entities): + logging.debug("Transaction : [{0}]".format(tx.raw)) + txid = self._transactions_processor.next_txid(connection.currency, blockstamp.number) + + tx_res, tx_entities[i] = await self._transactions_processor.send(tx, connection.currency) + + # Result can be negative if a tx is not accepted by the network + if result[0]: + if not tx_res[0]: + result = (False, tx_res[1]), tx_entities + result = result[0], tx_entities + return result except NotEnoughChangeError as e: - return (False, str(e)), None + return (False, str(e)), tx_entities diff --git a/tests/functional/test_certification_dialog.py b/tests/functional/test_certification_dialog.py index 29a0efa41d1ca3c0f28bd303099d96e914402a49..916086a6faf7f7b59cdda3c164558361b1f27371 100644 --- a/tests/functional/test_certification_dialog.py +++ b/tests/functional/test_certification_dialog.py @@ -38,7 +38,7 @@ async def test_certification_init_community(application_with_one_connection, fak QTest.keyClicks(certification_dialog.password_input.view.edit_password, bob.password) 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) + await asyncio.sleep(0.2) assert isinstance(fake_server.forge.pool[0], Certification) application_with_one_connection.loop.call_later(10, close_dialog) diff --git a/tests/technical/test_documents_service.py b/tests/technical/test_documents_service.py new file mode 100644 index 0000000000000000000000000000000000000000..67e0ca445f4344b8c6904cacf9b63722baaa113a --- /dev/null +++ b/tests/technical/test_documents_service.py @@ -0,0 +1,30 @@ +import pytest +from sakia.data.entities import Transaction + + +@pytest.mark.asyncio +async def test_send_more_than_40_sources(application_with_one_connection, fake_server, bob, alice): + for i in range(0, 60): + fake_server.forge.generate_dividend() + fake_server.forge.forge_block() + + new_blocks = fake_server.forge.blocks[-60:] + changed_tx, new_tx, new_ud = await application_with_one_connection.transactions_service.handle_new_blocks(new_blocks) + + await application_with_one_connection.sources_service.refresh_sources_of_pubkey(bob.key.pubkey, new_tx, new_ud, None) + amount_before_send = application_with_one_connection.sources_service.amount(bob.key.pubkey) + bob_connection = application_with_one_connection.db.connections_repo.get_one(pubkey=bob.key.pubkey) + + result, transactions = await application_with_one_connection.documents_service.send_money(bob_connection, + bob.salt, + bob.password, + alice.key.pubkey, + amount_before_send, + 0, + "Test comment") + assert transactions[0].comment == "[CHAINED]" + assert transactions[1].comment == "Test comment" + amount_after_send = application_with_one_connection.sources_service.amount(bob.key.pubkey) + assert amount_after_send == 0 + + await fake_server.close()