Skip to content
Snippets Groups Projects
Select Git revision
  • 86f8b2f665b88ae36353a6ead0945a4928a8b3b2
  • main default protected
  • release/1.1
  • encrypt_comments
  • mnemonic_dewif
  • authors_rules
  • 0.14
  • rtd
  • 1.2.1 protected
  • 1.2.0 protected
  • 1.1.1 protected
  • 1.1.0 protected
  • 1.0.0 protected
  • 1.0.0rc1 protected
  • 1.0.0rc0 protected
  • 1.0.0-rc protected
  • 0.62.0 protected
  • 0.61.0 protected
  • 0.60.1 protected
  • 0.58.1 protected
  • 0.60.0 protected
  • 0.58.0 protected
  • 0.57.0 protected
  • 0.56.0 protected
  • 0.55.1 protected
  • 0.55.0 protected
  • 0.54.3 protected
  • 0.54.2 protected
28 results

searchindex.js

Blame
  • transaction.py 24.91 KiB
    import re
    from typing import TypeVar, List, Any, Type, Optional, Dict, Union
    
    import pypeg2
    
    from duniterpy.grammars.output import Condition
    from .block_uid import BlockUID
    from .document import Document, MalformedDocumentError
    from ..constants import PUBKEY_REGEX, TRANSACTION_HASH_REGEX, BLOCK_ID_REGEX, BLOCK_UID_REGEX
    from ..grammars import output
    
    
    def reduce_base(amount: int, base: int) -> tuple:
        """
        Compute the reduced base of the given parameters
    
        :param amount: the amount value
        :param base: current base value
    
        :return: tuple containing computed (amount, base)
        """
        if amount == 0:
            return 0, 0
    
        next_amount = amount
        next_base = base
        next_amount_is_integer = True
        while next_amount_is_integer:
            amount = next_amount
            base = next_base
            if next_amount % 10 == 0:
                next_amount = int(next_amount / 10)
                next_base += 1
            else:
                next_amount_is_integer = False
    
        return int(amount), int(base)
    
    
    # required to type hint cls in classmethod
    InputSourceType = TypeVar('InputSourceType', bound='InputSource')
    
    
    class InputSource:
        """
        A Transaction INPUT
    
    .. note:: Compact :
        INDEX:SOURCE:FINGERPRINT:AMOUNT
    
        """
        re_inline = re.compile(
            "(?:(?:(D):({pubkey_regex}):({block_id_regex}))|(?:(T):({transaction_hash_regex}):([0-9]+)))\n"
            .format(pubkey_regex=PUBKEY_REGEX,
                    block_id_regex=BLOCK_ID_REGEX,
                    transaction_hash_regex=TRANSACTION_HASH_REGEX))
        re_inline_v3 = re.compile(
            "([0-9]+):([0-9]):(?:(?:(D):({pubkey_regex}):({block_id_regex}))|(?:(T):({transaction_hash_regex}):\
    ([0-9]+)))"
            .format(pubkey_regex=PUBKEY_REGEX,
                    block_id_regex=BLOCK_ID_REGEX,
                    transaction_hash_regex=TRANSACTION_HASH_REGEX))
    
        def __init__(self, amount: int, base: int, source: str, origin_id: str, index: int) -> None:
            """
            An input source can come from a dividend or a transaction.
    
            :param amount: amount of the input
            :param base: base of the input
            :param source: D if dividend, T if transaction
            :param origin_id: a Public key if a dividend, a tx hash if a transaction
            :param index: a block id if a dividend, an tx index if a transaction
            :return:
            """
            self.amount = amount
            self.base = base
            self.source = source
            self.origin_id = origin_id
            self.index = index
    
        def __eq__(self, other: Any) -> bool:
            """
            Check InputSource instances equality
            """
            if not isinstance(other, InputSource):
                return NotImplemented
            return self.amount == other.amount and \
                self.base == other.base and \
                self.source == other.source and \
                self.origin_id == other.origin_id and \
                self.index == other.index
    
        def __hash__(self) -> int:
            return hash((self.amount, self.base, self.source, self.origin_id, self.index))
    
        @classmethod
        def from_inline(cls: Type[InputSourceType], tx_version: int, inline: str) -> InputSourceType:
            """
            Return Transaction instance from inline string format
    
            :param tx_version: Version number of the document
            :param inline: Inline string format
            :return:
            """
            if tx_version == 2:
                data = InputSource.re_inline.match(inline)
                if data is None:
                    raise MalformedDocumentError("Inline input")
                source_offset = 0
                amount = 0
                base = 0
            else:
                data = InputSource.re_inline_v3.match(inline)
                if data is None:
                    raise MalformedDocumentError("Inline input")
                source_offset = 2
                amount = int(data.group(1))
                base = int(data.group(2))
            if data.group(1 + source_offset):
                source = data.group(1 + source_offset)
                origin_id = data.group(2 + source_offset)
                index = int(data.group(3 + source_offset))
            else:
                source = data.group(4 + source_offset)
                origin_id = data.group(5 + source_offset)
                index = int(data.group(6 + source_offset))
    
            return cls(amount, base, source, origin_id, index)
    
        def inline(self, tx_version: int) -> str:
            """
            Return an inline string format of the document
    
            :param tx_version: Version number of the document
            :return:
            """
            if tx_version == 2:
                return "{0}:{1}:{2}".format(self.source,
                                            self.origin_id,
                                            self.index)
            else:
                return "{0}:{1}:{2}:{3}:{4}".format(self.amount,
                                                    self.base,
                                                    self.source,
                                                    self.origin_id,
                                                    self.index)
    
    
    # required to type hint cls in classmethod
    OutputSourceType = TypeVar('OutputSourceType', bound='OutputSource')
    
    
    class OutputSource:
        """
        A Transaction OUTPUT
        """
        re_inline = re.compile("([0-9]+):([0-9]):(.*)")
    
        def __init__(self, amount: int, base: int, condition: str) -> None:
            """
            Init OutputSource instance
    
            :param amount: Amount of the output
            :param base: Base number
            :param condition: Condition expression
            """
            self.amount = amount
            self.base = base
            self.condition = self.condition_from_text(condition)
    
        def __eq__(self, other: Any) -> bool:
            """
            Check OutputSource instances equality
            """
            if not isinstance(other, OutputSource):
                return NotImplemented
            return self.amount == other.amount and \
                self.base == other.base and \
                self.condition == other.condition
    
        def __hash__(self) -> int:
            return hash((self.amount, self.base, self.condition))
    
        @classmethod
        def from_inline(cls: Type[OutputSourceType], inline: str) -> OutputSourceType:
            """
            Return OutputSource instance from inline string format
    
            :param inline: Inline string format
            :return:
            """
            data = OutputSource.re_inline.match(inline)
            if data is None:
                raise MalformedDocumentError("Inline output")
            amount = int(data.group(1))
            base = int(data.group(2))
            condition_text = data.group(3)
    
            return cls(amount, base, condition_text)
    
        def inline(self) -> str:
            """
            Return an inline string format of the output source
    
            :return:
            """
            return "{0}:{1}:{2}".format(self.amount, self.base,
                                        pypeg2.compose(self.condition, output.Condition))
    
        def inline_condition(self) -> str:
            """
            Return an inline string format of the output source’s condition
    
            :return:
            """
            return pypeg2.compose(self.condition, output.Condition)
    
        @staticmethod
        def condition_from_text(text) -> Condition:
            """
            Return a Condition instance with PEG grammar from text
    
            :param text: PEG parsable string
            :return:
            """
            try:
                condition = pypeg2.parse(text, output.Condition)
            except SyntaxError:
                # Invalid conditions are possible, see https://github.com/duniter/duniter/issues/1156
                # In such a case, they are store as empty PEG grammar object and considered unlockable
                condition = Condition(text)
            return condition
    
    
    # required to type hint cls in classmethod
    SIGParameterType = TypeVar('SIGParameterType', bound='SIGParameter')
    
    
    class SIGParameter:
        """
        A Transaction UNLOCK SIG parameter
        """
        re_sig = re.compile("SIG\\(([0-9]+)\\)")
    
        def __init__(self, index: int) -> None:
            """
            Init SIGParameter instance
    
            :param index: Index in list
            """
            self.index = index
    
        @classmethod
        def from_parameter(cls: Type[SIGParameterType], parameter: str) -> Optional[SIGParameterType]:
            """
            Return a SIGParameter instance from an index parameter
    
            :param parameter: Index parameter
    
            :return:
            """
            sig = SIGParameter.re_sig.match(parameter)
            if sig:
                return cls(int(sig.group(1)))
    
            return None
    
        def __str__(self):
            """
            Return a string representation of the SIGParameter instance
    
            :return:
            """
            return "SIG({0})".format(self.index)
    
    
    # required to type hint cls in classmethod
    XHXParameterType = TypeVar('XHXParameterType', bound='XHXParameter')
    
    
    class XHXParameter:
        """
        A Transaction UNLOCK XHX parameter
        """
        re_xhx = re.compile("XHX\\(([0-9]+)\\)")
    
        def __init__(self, integer: int) -> None:
            """
            Init XHXParameter instance
    
            :param integer: XHX number
            """
            self.integer = integer
    
        @classmethod
        def from_parameter(cls: Type[XHXParameterType], parameter: str) -> Optional[XHXParameterType]:
            """
            Return a XHXParameter instance from an index parameter
    
            :param parameter: Index parameter
    
            :return:
            """
            xhx = XHXParameter.re_xhx.match(parameter)
            if xhx:
                return cls(int(xhx.group(1)))
    
            return None
    
        def compute(self):
            pass
    
        def __str__(self):
            """
            Return a string representation of the XHXParameter instance
    
            :return:
            """
            return "XHX({0})".format(self.integer)
    
    
    # required to type hint cls in classmethod
    UnlockParameterType = TypeVar('UnlockParameterType', bound='UnlockParameter')
    
    
    class UnlockParameter:
    
        @classmethod
        def from_parameter(cls: Type[UnlockParameterType], parameter: str) -> Optional[Union[SIGParameter, XHXParameter]]:
            """
            Return UnlockParameter instance from parameter string
    
            :param parameter: Parameter string
            :return:
            """
    
            sig_param = SIGParameter.from_parameter(parameter)
            if sig_param:
                return sig_param
            else:
                xhx_param = XHXParameter.from_parameter(parameter)
                if xhx_param:
                    return xhx_param
    
            return None
    
        def compute(self):
            pass
    
    
    # required to type hint cls in classmethod
    UnlockType = TypeVar('UnlockType', bound='Unlock')
    
    
    class Unlock:
        """
        A Transaction UNLOCK
        """
        re_inline = re.compile("([0-9]+):((?:SIG\\([0-9]+\\)|XHX\\([0-9]+\\)|\\s)+)\n")
    
        def __init__(self, index: int, parameters: List[Union[SIGParameter, XHXParameter]]) -> None:
            """
            Init Unlock instance
    
            :param index: Index number
            :param parameters: List of UnlockParameter instances
            """
            self.index = index
            self.parameters = parameters
    
        @classmethod
        def from_inline(cls: Type[UnlockType], inline: str) -> UnlockType:
            """
            Return an Unlock instance from inline string format
    
            :param inline: Inline string format
    
            :return:
            """
            data = Unlock.re_inline.match(inline)
            if data is None:
                raise MalformedDocumentError("Inline input")
            index = int(data.group(1))
            parameters_str = data.group(2).split(' ')
            parameters = []
            for p in parameters_str:
                param = UnlockParameter.from_parameter(p)
                if param:
                    parameters.append(param)
            return cls(index, parameters)
    
        def inline(self) -> str:
            """
            Return inline string format of the instance
    
            :return:
            """
            return "{0}:{1}".format(self.index, ' '.join([str(p) for p in self.parameters]))
    
    
    # required to type hint cls in classmethod
    TransactionType = TypeVar('TransactionType', bound='Transaction')
    
    
    class Transaction(Document):
        """
    .. note:: A transaction document is specified by the following format :
    
        | Document format :
        | Version: VERSION
        | Type: Transaction
        | Currency: CURRENCY_NAME
        | Issuers:
        | PUBLIC_KEY
        | ...
        | Inputs:
        | INDEX:SOURCE:NUMBER:FINGERPRINT:AMOUNT
        | ...
        | Outputs:
        | PUBLIC_KEY:AMOUNT
        | ...
        | Comment: COMMENT
        | ...
        |
        |
        | Compact format :
        | TX:VERSION:NB_ISSUERS:NB_INPUTS:NB_OUTPUTS:HAS_COMMENT
        | PUBLIC_KEY:INDEX
        | ...
        | INDEX:SOURCE:FINGERPRINT:AMOUNT
        | ...
        | PUBLIC_KEY:AMOUNT
        | ...
        | COMMENT
        | SIGNATURE
        | ...
    
        """
    
        re_type = re.compile("Type: (Transaction)\n")
        re_header = re.compile("TX:([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([01]):([0-9]+)\n")
        re_compact_blockstamp = re.compile("({block_uid_regex})\n".format(block_uid_regex=BLOCK_UID_REGEX))
        re_blockstamp = re.compile("Blockstamp: ({block_uid_regex})\n".format(block_uid_regex=BLOCK_UID_REGEX))
        re_locktime = re.compile("Locktime: ([0-9]+)\n")
        re_issuers = re.compile("Issuers:\n")
        re_inputs = re.compile("Inputs:\n")
        re_unlocks = re.compile("Unlocks:\n")
        re_outputs = re.compile("Outputs:\n")
        re_compact_comment = re.compile("([^\n]+)\n")
        re_comment = re.compile("Comment: ([^\n]*)\n")
        re_pubkey = re.compile("({pubkey_regex})\n".format(pubkey_regex=PUBKEY_REGEX))
    
        fields_parsers = {**Document.fields_parsers, **{
            "Type": re_type,
            "Blockstamp": re_blockstamp,
            "CompactBlockstamp": re_compact_blockstamp,
            "Locktime": re_locktime,
            "TX": re_header,
            "Issuers": re_issuers,
            "Inputs": re_inputs,
            "Unlocks": re_unlocks,
            "Outputs": re_outputs,
            "Comment": re_comment,
            "Compact comment": re_compact_comment,
            "Pubkey": re_pubkey
        }
                          }
    
        def __init__(self, version: int, currency: str, blockstamp: Optional[BlockUID], locktime: int, issuers: List[str],
                     inputs: List[InputSource], unlocks: List[Unlock], outputs: List[OutputSource],
                     comment: str, signatures: List[str]) -> None:
            """
            Init Transaction instance
    
            :param version: Version number of the document
            :param currency: Name of the currency
            :param blockstamp: BlockUID timestamp of the block
            :param locktime: Lock time in seconds
            :param issuers: List of issuers public key
            :param inputs: List of InputSource instances
            :param unlocks: List of Unlock instances
            :param outputs: List of OutputSource instances
            :param comment: Comment field
            :param signatures: List of signatures
            """
            super().__init__(version, currency, signatures)
            self.blockstamp = blockstamp
            self.locktime = locktime
            self.issuers = issuers
            self.inputs = inputs
            self.unlocks = unlocks
            self.outputs = outputs
            self.comment = comment
    
        @classmethod
        def from_bma_history(cls: Type[TransactionType], currency: str, tx_data: Dict) -> TransactionType:
            """
            Get the transaction instance from json
    
            :param currency: the currency of the tx
            :param tx_data: json data of the transaction
    
            :return:
            """
            tx_data = tx_data.copy()
            tx_data["currency"] = currency
            for data_list in ('issuers', 'outputs', 'inputs', 'unlocks', 'signatures'):
                tx_data['multiline_{0}'.format(data_list)] = '\n'.join(tx_data[data_list])
            if tx_data["version"] >= 3:
                signed_raw = """Version: {version}
    Type: Transaction
    Currency: {currency}
    Blockstamp: {blockstamp}
    Locktime: {locktime}
    Issuers:
    {multiline_issuers}
    Inputs:
    {multiline_inputs}
    Unlocks:
    {multiline_unlocks}
    Outputs:
    {multiline_outputs}
    Comment: {comment}
    {multiline_signatures}
    """.format(**tx_data)
            else:
                signed_raw = """Version: {version}
    Type: Transaction
    Currency: {currency}
    Locktime: {locktime}
    Issuers:
    {multiline_issuers}
    Inputs:
    {multiline_inputs}
    Unlocks:
    {multiline_unlocks}
    Outputs:
    {multiline_outputs}
    Comment: {comment}
    {multiline_signatures}
    """.format(**tx_data)
            return cls.from_signed_raw(signed_raw)
    
        @classmethod
        def from_compact(cls: Type[TransactionType], currency: str, compact: str) -> TransactionType:
            """
            Return Transaction instance from compact string format
    
            :param currency: Name of the currency
            :param compact: Compact format string
            :return:
            """
            lines = compact.splitlines(True)
            n = 0
    
            header_data = Transaction.re_header.match(lines[n])
            if header_data is None:
                raise MalformedDocumentError("Compact TX header")
            version = int(header_data.group(1))
            issuers_num = int(header_data.group(2))
            inputs_num = int(header_data.group(3))
            unlocks_num = int(header_data.group(4))
            outputs_num = int(header_data.group(5))
            has_comment = int(header_data.group(6))
            locktime = int(header_data.group(7))
            n += 1
    
            blockstamp = None  # type: Optional[BlockUID]
            if version >= 3:
                blockstamp = BlockUID.from_str(Transaction.parse_field("CompactBlockstamp", lines[n]))
                n += 1
    
            issuers = []
            inputs = []
            unlocks = []
            outputs = []
            signatures = []
            for i in range(0, issuers_num):
                issuer = Transaction.parse_field("Pubkey", lines[n])
                issuers.append(issuer)
                n += 1
    
            for i in range(0, inputs_num):
                input_source = InputSource.from_inline(version, lines[n])
                inputs.append(input_source)
                n += 1
    
            for i in range(0, unlocks_num):
                unlock = Unlock.from_inline(lines[n])
                unlocks.append(unlock)
                n += 1
    
            for i in range(0, outputs_num):
                output_source = OutputSource.from_inline(lines[n])
                outputs.append(output_source)
                n += 1
    
            comment = ""
            if has_comment == 1:
                data = Transaction.re_compact_comment.match(lines[n])
                if data:
                    comment = data.group(1)
                    n += 1
                else:
                    raise MalformedDocumentError("Compact TX Comment")
    
            while n < len(lines):
                data = Transaction.re_signature.match(lines[n])
                if data:
                    signatures.append(data.group(1))
                    n += 1
                else:
                    raise MalformedDocumentError("Compact TX Signatures")
    
            return cls(version, currency, blockstamp, locktime, issuers, inputs, unlocks, outputs, comment, signatures)
    
        @classmethod
        def from_signed_raw(cls: Type[TransactionType], raw: str) -> TransactionType:
            """
            Return a Transaction instance from a raw string format
    
            :param raw: Raw string format
    
            :return:
            """
            lines = raw.splitlines(True)
            n = 0
    
            version = int(Transaction.parse_field("Version", lines[n]))
            n += 1
    
            Transaction.parse_field("Type", lines[n])
            n += 1
    
            currency = Transaction.parse_field("Currency", lines[n])
            n += 1
    
            blockstamp = None  # type: Optional[BlockUID]
            if version >= 3:
                blockstamp = BlockUID.from_str(Transaction.parse_field("Blockstamp", lines[n]))
                n += 1
    
            locktime = Transaction.parse_field("Locktime", lines[n])
            n += 1
    
            issuers = []
            inputs = []
            unlocks = []
            outputs = []
            signatures = []
    
            if Transaction.re_issuers.match(lines[n]):
                n += 1
                while Transaction.re_inputs.match(lines[n]) is None:
                    issuer = Transaction.parse_field("Pubkey", lines[n])
                    issuers.append(issuer)
                    n += 1
    
            if Transaction.re_inputs.match(lines[n]):
                n += 1
                while Transaction.re_unlocks.match(lines[n]) is None:
                    input_source = InputSource.from_inline(version, lines[n])
                    inputs.append(input_source)
                    n += 1
    
            if Transaction.re_unlocks.match(lines[n]):
                n += 1
                while Transaction.re_outputs.match(lines[n]) is None:
                    unlock = Unlock.from_inline(lines[n])
                    unlocks.append(unlock)
                    n += 1
    
            if Transaction.re_outputs.match(lines[n]) is not None:
                n += 1
                while not Transaction.re_comment.match(lines[n]):
                    _output = OutputSource.from_inline(lines[n])
                    outputs.append(_output)
                    n += 1
    
            comment = Transaction.parse_field("Comment", lines[n])
            n += 1
    
            if Transaction.re_signature.match(lines[n]) is not None:
                while n < len(lines):
                    sign = Transaction.parse_field("Signature", lines[n])
                    signatures.append(sign)
                    n += 1
    
            return cls(version, currency, blockstamp, locktime, issuers, inputs, unlocks, outputs,
                       comment, signatures)
    
        def raw(self) -> str:
            """
            Return raw string format from the instance
    
            :return:
            """
            doc = """Version: {0}
    Type: Transaction
    Currency: {1}
    """.format(self.version,
               self.currency)
    
            if self.version >= 3:
                doc += "Blockstamp: {0}\n".format(self.blockstamp)
    
            doc += "Locktime: {0}\n".format(self.locktime)
    
            doc += "Issuers:\n"
            for p in self.issuers:
                doc += "{0}\n".format(p)
    
            doc += "Inputs:\n"
            for i in self.inputs:
                doc += "{0}\n".format(i.inline(self.version))
    
            doc += "Unlocks:\n"
            for u in self.unlocks:
                doc += "{0}\n".format(u.inline())
    
            doc += "Outputs:\n"
            for o in self.outputs:
                doc += "{0}\n".format(o.inline())
    
            doc += "Comment: "
            doc += "{0}\n".format(self.comment)
    
            return doc
    
        def compact(self) -> str:
            """
            Return a transaction in its compact format from the instance
    
            :return:
            """
            """TX:VERSION:NB_ISSUERS:NB_INPUTS:NB_UNLOCKS:NB_OUTPUTS:HAS_COMMENT:LOCKTIME
    PUBLIC_KEY:INDEX
    ...
    INDEX:SOURCE:FINGERPRINT:AMOUNT
    ...
    PUBLIC_KEY:AMOUNT
    ...
    COMMENT
    """
            doc = "TX:{0}:{1}:{2}:{3}:{4}:{5}:{6}\n".format(self.version,
                                                            len(self.issuers),
                                                            len(self.inputs),
                                                            len(self.unlocks),
                                                            len(self.outputs),
                                                            '1' if self.comment != "" else '0',
                                                            self.locktime)
            if self.version >= 3:
                doc += "{0}\n".format(self.blockstamp)
    
            for pubkey in self.issuers:
                doc += "{0}\n".format(pubkey)
            for i in self.inputs:
                doc += "{0}\n".format(i.inline(self.version))
            for u in self.unlocks:
                doc += "{0}\n".format(u.inline())
            for o in self.outputs:
                doc += "{0}\n".format(o.inline())
            if self.comment != "":
                doc += "{0}\n".format(self.comment)
            for s in self.signatures:
                doc += "{0}\n".format(s)
    
            return doc
    
    
    class SimpleTransaction(Transaction):
        """
        As transaction class, but for only one issuer.
        ...
        """
    
        def __init__(self, version: int, currency: str, blockstamp: BlockUID, locktime: int, issuer: str,
                     single_input: InputSource, unlocks: List[Unlock], outputs: List[OutputSource], comment: str,
                     signature: str) -> None:
            """
            Init instance
    
            :param version: Version number of the document
            :param currency: Name of the currency
            :param blockstamp: BlockUID timestamp
            :param locktime: Lock time in seconds
            :param issuer: Issuer public key
            :param single_input: InputSource instance
            :param unlocks: List of Unlock instances
            :param outputs: List of OutputSource instances
            :param comment: Comment field
            :param signature: Signature
            """
            super().__init__(version, currency, blockstamp, locktime, [issuer], [single_input], unlocks,
                             outputs, comment, [signature])
    
        @staticmethod
        def is_simple(tx: Transaction) -> bool:
            """
            Filter a transaction and checks if it is a basic one
            A simple transaction is a tx which has only one issuer
            and two outputs maximum. The unlocks must be done with
            simple "SIG" functions, and the outputs must be simple
            SIG conditions.
    
            :param tx: the transaction to check
    
            :return: True if a simple transaction
            """
            simple = True
            if len(tx.issuers) != 1:
                simple = False
            for unlock in tx.unlocks:
                if len(unlock.parameters) != 1:
                    simple = False
                elif type(unlock.parameters[0]) is not SIGParameter:
                    simple = False
            for o in tx.outputs:
                # if right condition is not None...
                if getattr(o.condition, 'right', None):
                    simple = False
                    # if left is not SIG...
                elif type(o.condition.left) is not output.SIG:
                    simple = False
    
            return simple