mirror of
https://github.com/aljazceru/python-teos.git
synced 2025-12-17 14:14:22 +01:00
135 lines
6.1 KiB
Python
135 lines
6.1 KiB
Python
from pisa.rpc_errors import *
|
|
from common.logger import Logger
|
|
from pisa.tools import bitcoin_cli
|
|
from pisa.utils.auth_proxy import JSONRPCException
|
|
from pisa.errors import UNKNOWN_JSON_RPC_EXCEPTION, RPC_TX_REORGED_AFTER_BROADCAST
|
|
|
|
logger = Logger("Carrier")
|
|
|
|
# FIXME: This class is not fully covered by unit tests
|
|
|
|
|
|
class Receipt:
|
|
"""
|
|
The :class:`Receipt` class represent the interaction between the :obj:`Carrier` and ``bitcoind`` when broadcasting
|
|
transactions. It is used to signal whether or not a transaction has been successfully broadcast and why.
|
|
|
|
Args:
|
|
delivered (:obj:`bool`): whether or not the transaction has been successfully broadcast.
|
|
confirmations (:obj:`int`): the number of confirmations of the transaction to broadcast. In certain situations
|
|
the :obj:`Carrier` may fail to broadcast a transaction because it was already in the blockchain.
|
|
This attribute signals those situations.
|
|
reason (:obj:`int`): an error code describing why the transaction broadcast failed.
|
|
|
|
Returns:
|
|
:obj:`Receipt`: A receipt describing whether or not the transaction was delivered. Notice that transactions
|
|
that are already on chain are flagged as delivered with a ``confirmations > 0`` whereas new transactions are so
|
|
with ``confirmations = 0``.
|
|
"""
|
|
|
|
def __init__(self, delivered, confirmations=0, reason=None):
|
|
self.delivered = delivered
|
|
self.confirmations = confirmations
|
|
self.reason = reason
|
|
|
|
|
|
class Carrier:
|
|
"""
|
|
The :class:`Carrier` is the class in charge of interacting with ``bitcoind`` to send/get transactions. It uses
|
|
:obj:`Receipt` objects to report about the sending outcome.
|
|
"""
|
|
|
|
# NOTCOVERED
|
|
def send_transaction(self, rawtx, txid):
|
|
"""
|
|
Tries to send a given raw transaction to the Bitcoin network using ``bitcoind``.
|
|
|
|
Args:
|
|
rawtx (:obj:`str`): a (potentially) signed raw transaction ready to be broadcast.
|
|
txid (:obj:`str`): the transaction id corresponding to ``rawtx``.
|
|
|
|
Returns:
|
|
:obj:`Receipt`: A receipt reporting whether the transaction was successfully delivered or not and why.
|
|
"""
|
|
|
|
try:
|
|
logger.info("Pushing transaction to the network", txid=txid, rawtx=rawtx)
|
|
bitcoin_cli().sendrawtransaction(rawtx)
|
|
|
|
receipt = Receipt(delivered=True)
|
|
|
|
except JSONRPCException as e:
|
|
errno = e.error.get("code")
|
|
# Since we're pushing a raw transaction to the network we can face several rejections
|
|
if errno == RPC_VERIFY_REJECTED:
|
|
# DISCUSS: 37-transaction-rejection
|
|
# TODO: UNKNOWN_JSON_RPC_EXCEPTION is not the proper exception here. This is long due.
|
|
receipt = Receipt(delivered=False, reason=UNKNOWN_JSON_RPC_EXCEPTION)
|
|
|
|
elif errno == RPC_VERIFY_ERROR:
|
|
# DISCUSS: 37-transaction-rejection
|
|
receipt = Receipt(delivered=False, reason=RPC_VERIFY_ERROR)
|
|
logger.error("Transaction couldn't be broadcast", error=e.error)
|
|
|
|
elif errno == RPC_VERIFY_ALREADY_IN_CHAIN:
|
|
logger.info("Transaction is already in the blockchain. Getting confirmation count", txid=txid)
|
|
|
|
# If the transaction is already in the chain, we get the number of confirmations and watch the tracker
|
|
# until the end of the appointment
|
|
tx_info = self.get_transaction(txid)
|
|
|
|
if tx_info is not None:
|
|
confirmations = int(tx_info.get("confirmations"))
|
|
receipt = Receipt(delivered=True, confirmations=confirmations, reason=RPC_VERIFY_ALREADY_IN_CHAIN)
|
|
|
|
else:
|
|
# There's a really unlikely edge case where a transaction can be reorged between receiving the
|
|
# notification and querying the data. Notice that this implies the tx being also kicked off the
|
|
# mempool, which again is really unlikely.
|
|
receipt = Receipt(delivered=False, reason=RPC_TX_REORGED_AFTER_BROADCAST)
|
|
|
|
elif errno == RPC_DESERIALIZATION_ERROR:
|
|
# Adding this here just for completeness. We should never end up here. The Carrier only sends txs
|
|
# handed by the Responder, who receives them from the Watcher, who checks that the tx can be properly
|
|
# deserialized
|
|
logger.info("Transaction cannot be deserialized".format(txid))
|
|
receipt = Receipt(delivered=False, reason=RPC_DESERIALIZATION_ERROR)
|
|
|
|
else:
|
|
# If something else happens (unlikely but possible) log it so we can treat it in future releases
|
|
logger.error("JSONRPCException", method="Carrier.send_transaction", error=e.error)
|
|
receipt = Receipt(delivered=False, reason=UNKNOWN_JSON_RPC_EXCEPTION)
|
|
|
|
return receipt
|
|
|
|
@staticmethod
|
|
def get_transaction(txid):
|
|
"""
|
|
Queries transaction data to ``bitcoind`` given a transaction id.
|
|
|
|
Args:
|
|
txid (:obj:`str`): a 32-byte hex-formatted string representing the transaction id.
|
|
|
|
Returns:
|
|
:obj:`dict` or :obj:`None`: A dictionary with the transaction data if the transaction can be found on the
|
|
chain.
|
|
Returns ``None`` otherwise.
|
|
"""
|
|
|
|
try:
|
|
tx_info = bitcoin_cli().getrawtransaction(txid, 1)
|
|
|
|
except JSONRPCException as e:
|
|
tx_info = None
|
|
# While it's quite unlikely, the transaction that was already in the blockchain could have been
|
|
# reorged while we were querying bitcoind to get the confirmation count. In such a case we just
|
|
# restart the tracker
|
|
if e.error.get("code") == RPC_INVALID_ADDRESS_OR_KEY:
|
|
logger.info("Transaction not found in mempool nor blockchain", txid=txid)
|
|
|
|
else:
|
|
# If something else happens (unlikely but possible) log it so we can treat it in future releases
|
|
logger.error("JSONRPCException", method="Carrier.get_transaction", error=e.error)
|
|
|
|
return tx_info
|