Something went wrong on our end
"""
Created on 31 janv. 2015
@author: inso
"""
import logging
import asyncio
from ucoinpy.api import bma
from ucoinpy.documents import Block, BlockId
from PyQt5.QtCore import pyqtSignal, QObject
import hashlib
from enum import Enum
class TransferState(Enum):
"""
TO_SEND means the transaction wasn't sent yet
AWAITING means the transaction is waiting to reach K blockchain validation
VALIDATED means the transaction was validated locally and is considered present in the blockchain
REFUSED means the transaction took too long to be registered in the blockchain,
therefore it is considered as refused
DROPPED means the transaction was canceled locally. It can still be validated
in the blockchain if it was sent, if the guy is unlucky ;)
"""
TO_SEND = 0
AWAITING = 1
VALIDATING = 4
VALIDATED = 2
REFUSED = 3
DROPPED = 5
class Transfer(QObject):
"""
A transfer is the lifecycle of a transaction.
"""
transfer_broadcasted = pyqtSignal(str)
broadcast_error = pyqtSignal(int, str)
def __init__(self, sha_hash, state, blockid, metadata, locally_created):
"""
The constructor of a transfer.
Check for metadata keys which must be present :
- receiver
- block
- time
- issuer
- amount
- comment
:param str sha_hash: The hash of the transaction
:param TransferState state: The state of the Transfer
:param ucoinpy.documents.BlockId blockid: The blockid of the transaction in the blockchain
:param dict metadata: The transfer metadata
"""
assert('receiver' in metadata)
assert('time' in metadata)
assert('issuer' in metadata)
assert('amount' in metadata)
assert('comment' in metadata)
assert('issuer_uid' in metadata)
assert('receiver_uid' in metadata)
assert('txid' in metadata)
super().__init__()
self.sha_hash = sha_hash
self.state = state
self.blockid = blockid
self._locally_created = locally_created
self._metadata = metadata
self._table_states = {
(TransferState.TO_SEND, (list, Block)):
(
(self._broadcast_success, lambda l, b: self._wait(b), TransferState.AWAITING),
(lambda l,b: self._broadcast_failure(b), None, TransferState.REFUSED),
),
(TransferState.TO_SEND, ()):
((self._is_locally_created, self._drop, TransferState.DROPPED),),
(TransferState.AWAITING, (bool, Block)):
((self._found_in_block, lambda r, b: self._be_validating(b), TransferState.VALIDATING),),
(TransferState.AWAITING, (bool, Block, int, int)):
((self._not_found_in_blockchain, None, TransferState.REFUSED),),
(TransferState.VALIDATING, (bool, Block, int)):
((self._reached_enough_validation, None, TransferState.VALIDATED),),
(TransferState.VALIDATING, (bool, Block)):
((self._rollback_and_removed, lambda r, b: self._drop(), TransferState.DROPPED),),
(TransferState.VALIDATED, (bool, Block, int)):
((self._rollback_in_fork_window, lambda r, b: self._be_validating(b), TransferState.VALIDATING),),
(TransferState.VALIDATED, (bool, Block)):
(
(self._rollback_and_removed, lambda r, b: self._drop(), TransferState.DROPPED),
(self._rollback_and_local, lambda r, b: self._wait(b), TransferState.AWAITING),
),
(TransferState.REFUSED, ()):
((self._is_locally_created, self._drop, TransferState.DROPPED),)
}
@classmethod
def initiate(cls, metadata):
"""
Create a new transfer in a "TO_SEND" state.
:param dict metadata: The computed metadata of the transfer
:return: A new transfer
:rtype: Transfer
"""
return cls(None, TransferState.TO_SEND, None, metadata, True)
@classmethod
def create_from_blockchain(cls, hash, blockid, metadata):
"""
Create a new transfer sent from another cutecoin instance
:param str hash: The transaction hash
:param ucoinpy.documents.BlockId blockid: The block id were we found the tx
:param dict metadata: The computed metadata of the transaction
:return: A new transfer
:rtype: Transfer
"""
return cls(hash, TransferState.VALIDATING, blockid, metadata, False)
@classmethod
def load(cls, data):
"""
Create a new transfer from a dict in json format.
:param dict data: The loaded data
:return: A new transfer
:rtype: Transfer
"""
return cls(data['hash'],
TransferState[data['state']],
BlockId.from_str(data['blockid']) if data['blockid'] else None,
data['metadata'], data['local'])
def jsonify(self):
"""
:return: The transfer as a dict in json format
"""
return {'hash': self.sha_hash,
'state': self.state.name,
'blockid': str(self.blockid) if self.blockid else None,
'metadata': self._metadata,
'local': self._locally_created}
@property
def metadata(self):
"""
:return: this transfer metadata
"""
return self._metadata
def _not_found_in_blockchain(self, rollback, block, mediantime_target, mediantime_blocks):
"""
Check if the transaction could not be found in the blockchain
:param bool rollback: True if we are in a rollback procedure
:param ucoinpy.documents.Block block: The block to look for the tx
:param int mediantime_target: The mediantime to mine a block in the community parameters
:param int mediantime_blocks: The number of block used to derive the mediantime
:return: True if the transaction could not be found in a given time
:rtype: bool
"""
if not rollback:
for tx in block.transactions:
if tx.sha_hash == self.sha_hash:
return False
if block.time > self.metadata['time'] + mediantime_target*mediantime_blocks:
return True
return False
def _found_in_block(self, rollback, block):
"""
Check if the transaction can be found in the blockchain
:param bool rollback: True if we are in a rollback procedure
:param ucoinpy.documents.Block block: The block to check for the transaction
:return: True if the transaction was found
:rtype: bool
"""
if not rollback:
for tx in block.transactions:
if tx.sha_hash == self.sha_hash:
return True
return False
def _broadcast_success(self, ret_codes, block):
"""
Check if the retcode is 200 after a POST
:param list ret_codes: The POST return codes of the broadcast
:param ucoinpy.documents.Block block: The current block used for transition.
:return: True if the post was successful
:rtype: bool
"""
return 200 in ret_codes
def _broadcast_failure(self, ret_codes):
"""
Check if no retcode is 200 after a POST
:param list ret_codes: The POST return codes of the broadcast
:return: True if the post was failed
:rtype: bool
"""
return 200 not in ret_codes
def _reached_enough_validation(self, rollback, current_block, fork_window):
"""
Check if the transfer reached enough validation in the blockchain
:param bool rollback: True if we are in a rollback procedure
:param ucoinpy.documents.Block current_block: The current block of the main blockchain
:param int fork_window: The number of validations needed on the network
:return: True if the transfer reached enough validations
:rtype: bool
"""
return not rollback and self.blockid.number + fork_window <= current_block.number
def _rollback_and_removed(self, rollback, block):
"""
Check if the transfer is not in the block anymore
:param bool rollback: True if we are in a rollback procedure
:param ucoinpy.documents.Block block: The block to check for the transaction
:return: True if the transfer is not found in the block
"""
if rollback:
if not block or block.blockid != self.blockid:
return True
else:
return self.sha_hash not in [t.sha_hash for t in block.transactions]
return False
def _rollback_in_fork_window(self, rollback, current_block, fork_window):
"""
Check if the transfer is not in the block anymore
:param bool rollback: True if we are in a rollback procedure
:param ucoinpy.documents.Block current_block: The block to check for the transaction
:return: True if the transfer is found in the block
"""
if rollback:
return self.blockid.number + fork_window > current_block.number
return False
def _rollback_and_local(self, rollback, block):
"""
Check if the transfer is not in the block anymore
:param bool rollback: True if we are in a rollback procedure
:param ucoinpy.documents.Block block: The block to check for the transaction
:return: True if the transfer is found in the block
"""
if rollback and self._locally_created and block.blockid == self.blockid:
return self.sha_hash not in [t.sha_hash for t in block.transactions]
return False
def _is_locally_created(self):
"""
Check if we can send back the transaction if it was locally created
:return: True if the transaction was locally created
"""
return self._locally_created
def _wait(self, current_block):
"""
Set the transfer as AWAITING validation.
:param ucoinpy.documents.Block current_block: Current block of the main blockchain
"""
self.blockid = current_block.blockid
self._metadata['time'] = current_block.mediantime
def _be_validating(self, block):
"""
Action when the transfer ins found in a block
:param bool rollback: True if we are in a rollback procedure
:param ucoinpy.documents.Block block: The block checked
"""
self.blockid = block.blockid
self._metadata['time'] = block.mediantime
def _drop(self):
"""
Cancel the transfer locally.
The transfer state becomes TransferState.DROPPED.
"""
self.blockid = None
def _try_transition(self, transition_key, inputs):
"""
Try the transition defined by the given transition_key
with inputs
:param tuple transition_key: The transition key in the table states
:param tuple inputs: The inputs
:return: True if the transition was applied
:rtype: bool
"""
if len(inputs) == len(transition_key[1]):
for i, input in enumerate(inputs):
if type(input) is not transition_key[1][i]:
return False
for transition in self._table_states[transition_key]:
if transition[0](*inputs):
logging.debug("{0} : {1} --> {2}".format(self.sha_hash[:5], self.state.name,
transition[2].name))
# If the transition changes data, apply changes
if transition[1]:
transition[1](*inputs)
self.state = transition[2]
return True
return False
def run_state_transitions(self, inputs):
"""
Try all current state transitions with inputs
:param tuple inputs: The inputs passed to the transitions
:return: True if the transaction changed state
:rtype: bool
"""
transition_keys = [k for k in self._table_states.keys() if k[0] == self.state]
for key in transition_keys:
if self._try_transition(key, inputs):
return True
return False
def cancel(self):
"""
Cancel a local transaction
"""
self.run_state_transitions(())
@asyncio.coroutine
def send(self, txdoc, community):
"""
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 txdoc: A transaction ucoinpy object
:param community: The community target of the transaction
"""
responses = yield from community.bma_access.broadcast(bma.tx.Process,
post_args={'transaction': txdoc.signed_raw()})
self.sha_hash = txdoc.sha_hash
blockid = yield from community.blockid()
block = yield from community.bma_access.future_request(bma.blockchain.Block,
req_args={'number': blockid.number})
signed_raw = "{0}{1}\n".format(block['raw'], block['signature'])
block_doc = Block.from_signed_raw(signed_raw)
result = (False, "")
for r in responses:
if r.status == 200:
result = (True, (yield from r.json()))
elif not result[0]:
result = (False, (yield from r.text()))
else:
yield from r.text()
self.run_state_transitions(([r.status for r in responses], block_doc))
self.run_state_transitions(([r.status for r in responses]))
return result