chainqueue/chainqueue/db/models/tx.py

247 lines
9.6 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# standard imports
import datetime
# external imports
from sqlalchemy import Column, String, Integer, DateTime, Enum, ForeignKey, Boolean, NUMERIC
from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property
from hexathon import (
strip_0x,
)
# local imports
from .base import SessionBase
from .otx import Otx
from chainqueue.error import (
NotLocalTxError,
TxStateChangeError,
)
class TxCache(SessionBase):
"""Metadata expansions for outgoing transactions.
These records are not essential for handling of outgoing transaction queues. It is implemented to reduce the amount of computation spent of parsing and analysing raw signed transaction data.
Instantiation of the object will fail if an outgoing transaction record with the same transaction hash does not exist.
Typically three types of transactions are recorded:
- Token transfers; where source and destination token values and addresses are identical, sender and recipient differ.
- Token conversions; source and destination token values and addresses differ, sender and recipient are identical.
- Any other transaction; source and destination token addresses are zero-address.
:param tx_hash: Transaction hash
:type tx_hash: str, 0x-hex
:param sender: Ethereum address of transaction sender
:type sender: str, 0x-hex
:param recipient: Ethereum address of transaction beneficiary (e.g. token transfer recipient)
:type recipient: str, 0x-hex
:param source_token_address: Contract address of token that sender spent from
:type source_token_address: str, 0x-hex
:param destination_token_address: Contract address of token that recipient will receive balance of
:type destination_token_address: str, 0x-hex
:param from_value: Amount of source tokens spent
:type from_value: number
:param to_value: Amount of destination tokens received
:type to_value: number
:param block_number: Block height the transaction was mined at, or None if not yet mined
:type block_number: number or None
:param tx_number: Transaction index in the block the transaction was mined in, or None if not yet mined
:type tx_number: number or None
:raises NotLocalTxError: Outgoing transaction for given transaction hash does not exist
"""
__tablename__ = 'tx_cache'
otx_id = Column(Integer, ForeignKey('otx.id'))
"""Foreign key to chainqueue.db.models.otx.Otx"""
source_token_address = Column(String())
"""Contract address of token that sender spent from"""
destination_token_address = Column(String())
"""Contract address of token that recipient will receive balance of"""
sender = Column(String())
"""Ethereum address of transaction sender"""
recipient = Column(String())
"""Ethereum address of transaction beneficiary (e.g. token transfer recipient)"""
from_value = Column(NUMERIC())
"""Amount of source tokens spent"""
to_value = Column(NUMERIC())
"""Amount of destination tokens received"""
tx_index = Column(Integer())
"""Transaction index in the block the transaction was mined in, or None if not yet mined"""
date_created = Column(DateTime, default=datetime.datetime.utcnow)
"""Datetime record was created"""
date_updated = Column(DateTime, default=datetime.datetime.utcnow)
"""Datetime record was last updated"""
date_checked = Column(DateTime, default=datetime.datetime.utcnow)
"""Datetime record was last checked for state change"""
def __init__(self, tx_hash, sender, recipient, source_token_address, destination_token_address, from_value, to_value, block_number=None, tx_index=None, session=None):
session = SessionBase.bind_session(session)
q = session.query(Otx)
q = q.filter(Otx.tx_hash==strip_0x(tx_hash))
tx = q.first()
if tx == None:
SessionBase.release_session(session)
raise NotLocalTxError('outgoing transaction record unknown {} (add a Tx first)'.format(tx_hash))
self.otx_id = tx.id
self.sender = sender
self.recipient = recipient
self.source_token_address = source_token_address
self.destination_token_address = destination_token_address
self.from_value = from_value
self.to_value = to_value
self.date_created = datetime.datetime.utcnow()
self.date_updated = self.date_created
self.date_checked = self.date_created
if tx_index != None and block_number != tx.block:
raise ValueError('Block number in argument {} does not match otx record {}'.format(block_number, tx.block))
self.tx_index = tx_index
SessionBase.release_session(session)
def check(self):
"""Update the "checked" timestamp to current time.
Only manipulates object, does not transaction or commit to backend.
:todo: evaluate whether this should consume a session like other methods
"""
self.date_checked = datetime.datetime.utcnow()
@staticmethod
def clone(
tx_hash_original,
tx_hash_new,
session=None,
):
"""Copy tx cache data and associate it with a new transaction.
:param tx_hash_original: tx cache data to copy
:type tx_hash_original: str, 0x-hex
:param tx_hash_new: tx hash to associate the copied entry with
:type tx_hash_new: str, 0x-hex
:param session: Sqlalchemy database session
:type session: sqlalchemy.orm.Session
:raise NotLocalTxError: Transaction does not exist in the local queue, or could not be created
:raies TxStateChangeError: Attempt to clone an already confirmed transaction
"""
session = SessionBase.bind_session(session)
q = session.query(TxCache)
q = q.join(Otx)
q = q.filter(Otx.tx_hash==strip_0x(tx_hash_original))
txc = q.first()
if txc == None:
SessionBase.release_session(session)
raise NotLocalTxError('original {}'.format(tx_hash_original))
if txc.tx_index != None:
SessionBase.release_session(session)
raise TxStateChangeError('cannot clone tx cache of confirmed tx {}'.format(tx_hash_original))
session.flush()
otx = Otx.load(tx_hash_new, session=session)
if otx == None:
SessionBase.release_session(session)
raise NotLocalTxError('new {}'.format(tx_hash_new))
txc_new = TxCache(
otx.tx_hash,
txc.sender,
txc.recipient,
txc.source_token_address,
txc.destination_token_address,
int(txc.from_value),
int(txc.to_value),
session=session,
)
session.add(txc_new)
session.commit()
SessionBase.release_session(session)
# TODO: possible dead code
@staticmethod
def account(account_address, session=None):
"""Retrieves all transaction hashes for which the given Ethereum address is sender or recipient.
:param account_address: Ethereum address to use in query.
:type account_address: str, 0x-hex
:param session: Sqlalchemy database session
:type session: sqlalchemy.orm.Session
:returns: Outgoing transactions
:rtype: tuple, where first element is transaction hash
"""
session = SessionBase.bind_session(session)
q = session.query(Otx.tx_hash)
q = q.join(TxCache)
q = q.filter(or_(TxCache.sender==account_address, TxCache.recipient==account_address))
txs = q.all()
SessionBase.release_session(session)
return list(txs)
@staticmethod
def set_final(tx_hash, block_number, tx_index, session=None):
"""Sets the transaction index for the confirmed transaction.
The block number of the block that included the transaction must be set in the otx record before this method is called (see chainqueue.db.models.otx.Otx.minefail and chainqueue.db.models.otx.Otx.success). The block number in the record must match the block number given as argument.
:param tx_hash: Transaction hash, in hex
:type tx_hash: str
:param block_number: Block number transaction was included in
:type block_number: int
:param tx_index: The transaction index of the block to cache
:type tx_index: int
:param session: Sqlalchemy database session
:type session: sqlalchemy.orm.Session
:raises NotLocalTxError: Transaction does not exist in local queue, or block column is not set in otx record.
"""
session = SessionBase.bind_session(session)
q = session.query(TxCache)
q = q.join(Otx)
q = q.filter(Otx.tx_hash==strip_0x(tx_hash))
q = q.filter(Otx.block==block_number)
o = q.first()
if o == None:
raise NotLocalTxError(tx_hash, block_number)
o.tx_index = tx_index
session.add(o)
session.flush()
SessionBase.release_session(session)
@staticmethod
def load(tx_hash, session=None):
"""Retrieves the outgoing transaction record by transaction hash.
:param tx_hash: Transaction hash
:type tx_hash: str, 0x-hex
:param session: Sqlalchemy database session
:type session: sqlalchemy.orm.Session
:rtype: chainqueue.db.models.tx.TxCache
:returns: Transaction cache object
"""
session = SessionBase.bind_session(session)
q = session.query(TxCache)
q = q.join(Otx)
q = q.filter(Otx.tx_hash==strip_0x(tx_hash))
SessionBase.release_session(session)
return q.first()