Skip to content
Snippets Groups Projects
Commit c705c805 authored by inso's avatar inso
Browse files

Chain transactions (fix #594)

parent 76efe7d9
Branches
Tags v1.36.4
No related merge requests found
...@@ -110,19 +110,17 @@ class TransactionsProcessor: ...@@ -110,19 +110,17 @@ class TransactionsProcessor:
""" """
return self.run_state_transitions(tx) 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. Send a transaction and update the transfer state to AWAITING if accepted.
If the transaction was refused (return code != 200), state becomes REFUSED If the transaction was refused (return code != 200), state becomes REFUSED
The txdoc is saved as the transfer txdoc. The txdoc is saved as the transfer txdoc.
:param sakia.data.entities.Transaction tx: the transaction :param sakia.data.entities.Transaction tx: the transaction
:param txdoc: A transaction duniterpy object
:param currency: The community target of the transaction :param currency: The community target of the transaction
""" """
self._logger.debug(txdoc.signed_raw())
self._repo.insert(tx) 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) result = await parse_bma_responses(responses)
self.run_state_transitions(tx, [r.status for r in responses if not isinstance(r, BaseException)]) self.run_state_transitions(tx, [r.status for r in responses if not isinstance(r, BaseException)])
return result, tx return result, tx
......
...@@ -146,7 +146,7 @@ class TransferController(QObject): ...@@ -146,7 +146,7 @@ class TransferController(QObject):
QApplication.setOverrideCursor(Qt.WaitCursor) QApplication.setOverrideCursor(Qt.WaitCursor)
logging.debug("Send money...") 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]: if result[0]:
await self.view.show_success(self.model.notifications(), recipient) await self.view.show_success(self.model.notifications(), recipient)
logging.debug("Restore cursor...") logging.debug("Restore cursor...")
...@@ -154,11 +154,13 @@ class TransferController(QObject): ...@@ -154,11 +154,13 @@ class TransferController(QObject):
# If we sent back a transaction we cancel the first one # If we sent back a transaction we cancel the first one
self.model.cancel_previous() 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() self.view.accept()
else: else:
await self.view.show_error(self.model.notifications(), result[1]) 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() QApplication.restoreOverrideCursor()
self.view.button_box.setEnabled(True) self.view.button_box.setEnabled(True)
......
...@@ -90,14 +90,15 @@ class TransferModel(QObject): ...@@ -90,14 +90,15 @@ class TransferModel(QObject):
:return: the result of the send :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) recipient, amount, amount_base, comment)
for transaction in transactions:
self.app.sources_service.parse_transaction(self.connection.pubkey, transaction) self.app.sources_service.parse_transaction(self.connection.pubkey, transaction)
if recipient in self._connections_processor.pubkeys(): if recipient in self._connections_processor.pubkeys():
self.app.sources_service.parse_transaction(recipient, transaction) self.app.sources_service.parse_transaction(recipient, transaction)
self.app.db.commit() self.app.db.commit()
self.app.sources_refreshed.emit() self.app.sources_refreshed.emit()
return result, transaction return result, transactions
def notifications(self): def notifications(self):
return self.app.parameters.notifications return self.app.parameters.notifications
......
...@@ -10,7 +10,7 @@ from duniterpy.documents import Transaction as TransactionDoc ...@@ -10,7 +10,7 @@ from duniterpy.documents import Transaction as TransactionDoc
from duniterpy.documents.transaction import reduce_base from duniterpy.documents.transaction import reduce_base
from duniterpy.grammars import output from duniterpy.grammars import output
from duniterpy.api import bma 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, \ from sakia.data.processors import BlockchainProcessor, IdentitiesProcessor, NodesProcessor, \
TransactionsProcessor, SourcesProcessor, CertificationsProcessor TransactionsProcessor, SourcesProcessor, CertificationsProcessor
from sakia.data.connectors import BmaConnector, parse_bma_responses from sakia.data.connectors import BmaConnector, parse_bma_responses
...@@ -326,10 +326,29 @@ class DocumentsService: ...@@ -326,10 +326,29 @@ class DocumentsService:
return total 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 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 str receiver: the target of the transaction
:param duniterpy.documents.BlockUID blockstamp: the blockstamp :param duniterpy.documents.BlockUID blockstamp: the blockstamp
:param int amount: the amount sent to the receiver :param int amount: the amount sent to the receiver
...@@ -337,23 +356,54 @@ class DocumentsService: ...@@ -337,23 +356,54 @@ class DocumentsService:
:param str message: the comment of the tx :param str message: the comment of the tx
:param str currency: the target community :param str currency: the target community
:return: the transaction document :return: the transaction document
:rtype: duniterpy.documents.Transaction :rtype: List[sakia.data.entities.Transaction]
""" """
result = self.tx_sources(int(amount), amount_base, currency, issuer) forged_tx = []
sources = [None]*41
while len(sources) > 40:
result = self.tx_sources(int(amount), amount_base, currency, key.pubkey)
sources = result[0] sources = result[0]
computed_outputs = result[1] computed_outputs = result[1]
overheads = result[2] 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) self._sources_processor.consume(sources)
logging.debug("Inputs : {0}".format(sources)) logging.debug("Inputs : {0}".format(sources))
inputs = self.tx_inputs(sources) inputs = self.tx_inputs(sources)
unlocks = self.tx_unlocks(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)) logging.debug("Outputs : {0}".format(outputs))
tx = TransactionDoc(10, currency, blockstamp, 0, txdoc = TransactionDoc(10, currency, blockstamp, 0,
[issuer], inputs, unlocks, [key.pubkey], inputs, unlocks,
outputs, message, None) outputs, message, None)
return tx 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): async def send_money(self, connection, secret_key, password, recipient, amount, amount_base, message):
""" """
...@@ -367,32 +417,25 @@ class DocumentsService: ...@@ -367,32 +417,25 @@ class DocumentsService:
:param str message: The message to send with the transfer :param str message: The message to send with the transfer
""" """
blockstamp = self._blockchain_processor.current_buid(connection.currency) blockstamp = self._blockchain_processor.current_buid(connection.currency)
time = self._blockchain_processor.time(connection.currency)
key = SigningKey(secret_key, password, connection.scrypt_params) key = SigningKey(secret_key, password, connection.scrypt_params)
logging.debug("Sender pubkey:{0}".format(key.pubkey)) logging.debug("Sender pubkey:{0}".format(key.pubkey))
tx_entities = []
result = (True, ""), tx_entities
try: try:
txdoc = self.prepare_tx(connection.pubkey, recipient, blockstamp, amount, amount_base, tx_entities = self.prepare_tx(key, recipient, blockstamp, amount, amount_base,
message, connection.currency) message, connection.currency)
logging.debug("TX : {0}".format(txdoc.raw()))
txdoc.sign([key]) for i, tx in enumerate(tx_entities):
logging.debug("Transaction : [{0}]".format(txdoc.signed_raw())) logging.debug("Transaction : [{0}]".format(tx.raw))
txid = self._transactions_processor.next_txid(connection.currency, blockstamp.number) txid = self._transactions_processor.next_txid(connection.currency, blockstamp.number)
tx = Transaction(currency=connection.currency,
sha_hash=txdoc.sha_hash, tx_res, tx_entities[i] = await self._transactions_processor.send(tx, connection.currency)
written_block=0,
blockstamp=blockstamp, # Result can be negative if a tx is not accepted by the network
timestamp=time, if result[0]:
signature=txdoc.signatures[0], if not tx_res[0]:
issuer=connection.pubkey, result = (False, tx_res[1]), tx_entities
receiver=recipient, result = result[0], tx_entities
amount=amount, return result
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)
except NotEnoughChangeError as e: except NotEnoughChangeError as e:
return (False, str(e)), None return (False, str(e)), tx_entities
...@@ -38,7 +38,7 @@ async def test_certification_init_community(application_with_one_connection, fak ...@@ -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) QTest.keyClicks(certification_dialog.password_input.view.edit_password, bob.password)
assert certification_dialog.view.button_box.button(QDialogButtonBox.Ok).isEnabled() assert certification_dialog.view.button_box.button(QDialogButtonBox.Ok).isEnabled()
QTest.mouseClick(certification_dialog.view.button_box.button(QDialogButtonBox.Ok), Qt.LeftButton) 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) assert isinstance(fake_server.forge.pool[0], Certification)
application_with_one_connection.loop.call_later(10, close_dialog) application_with_one_connection.loop.call_later(10, close_dialog)
......
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()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment