Astroport.ONE/venv/lib/python3.11/site-packages/duniterpy/documents/transaction.py

1059 lines
30 KiB
Python
Raw Normal View History

2024-03-01 16:15:45 +01:00
# Copyright 2014-2024 Vincent Texier <vit@free.fr>
#
# DuniterPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# DuniterPy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
import logging
import re
from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union
import pypeg2
from duniterpy.grammars.output import Condition
from ..constants import (
BLOCK_ID_REGEX,
BLOCK_NUMBER_REGEX,
G1_CURRENCY_CODENAME,
PUBKEY_REGEX,
TRANSACTION_HASH_REGEX,
)
from ..grammars import output
from ..key import SigningKey, VerifyingKey
from .block_id import BlockID
from .document import Document, MalformedDocumentError
VERSION = 10
def reduce_base(amount: int, base: int) -> Tuple[int, int]:
"""
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(
f"([0-9]+):([0-9]):(?:(?:(D):({PUBKEY_REGEX}):({BLOCK_NUMBER_REGEX}))|\
(?:(T):({TRANSACTION_HASH_REGEX}):([0-9]+)))"
)
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], inline: str) -> InputSourceType:
"""
Return Transaction instance from inline string format
:param inline: Inline string format
:return:
"""
data = InputSource.re_inline.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) -> str:
"""
Return an inline string format of the document
:return:
"""
return f"{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 f"{self.amount}:{self.base}:{pypeg2.compose(self.condition, output.Condition)}"
def inline_condition(self) -> str:
"""
Return an inline string format of the output sources 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
def __eq__(self, other: Any) -> bool:
"""
Check SIGParameter instances equality
"""
if not isinstance(other, SIGParameter):
return NotImplemented
return self.index == other.index
def __hash__(self) -> int:
return hash(self.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 f"SIG({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
def __eq__(self, other: Any) -> bool:
"""
Check XHXParameter instances equality
"""
if not isinstance(other, XHXParameter):
return NotImplemented
return self.integer == other.integer
def __hash__(self) -> int:
return hash(self.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 f"XHX({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:
"""
result = None # type: Optional[Union[SIGParameter, XHXParameter]]
sig_param = SIGParameter.from_parameter(parameter)
if sig_param:
result = sig_param
else:
xhx_param = XHXParameter.from_parameter(parameter)
if xhx_param:
result = xhx_param
return result
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)+)")
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
def __eq__(self, other: Any) -> bool:
"""
Check Unlock instances equality
"""
if not isinstance(other, Unlock):
return NotImplemented
params_equals = True
for spar, opar in zip(self.parameters, other.parameters):
if spar != opar:
params_equals = False
return self.index == other.index and params_equals
def __hash__(self) -> int:
return hash((self.index, self.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 parameter in parameters_str:
param = UnlockParameter.from_parameter(parameter)
if param:
parameters.append(param)
return cls(index, parameters)
def inline(self) -> str:
"""
Return inline string format of the instance
:return:
"""
params = " ".join([str(parameter) for parameter in self.parameters])
return f"{self.index}:{params}"
class SignatureException(Exception):
pass
# 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_block_id = re.compile(f"({BLOCK_ID_REGEX})\n")
re_block_id = re.compile(f"Blockstamp: ({BLOCK_ID_REGEX})\n")
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(f"({PUBKEY_REGEX})\n")
fields_parsers = {
**Document.fields_parsers,
**{
"Type": re_type,
"Blockstamp": re_block_id,
"CompactBlockstamp": re_compact_block_id,
"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,
block_id: Optional[BlockID],
locktime: int,
issuers: List[str],
inputs: List[InputSource],
unlocks: List[Unlock],
outputs: List[OutputSource],
comment: str,
time: Optional[int] = None,
signing_keys: Optional[Union[SigningKey, List[SigningKey]]] = None,
version: int = VERSION,
currency: str = G1_CURRENCY_CODENAME,
) -> None:
"""
Init Transaction instance
:param block_id: BlockID instance
: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 time: time when the transaction enters the blockchain
:param signing_keys: SigningKey or list of SigningKey instances to sign the document (default=None)
:param version: Document version (default=transaction.VERSION)
:param currency: Currency codename (default=constants.CURRENCY_CODENAME_G1)
"""
super().__init__(version, currency)
self.block_id = block_id
self.locktime = locktime
self.issuers = issuers
self.inputs = inputs
self.unlocks = unlocks
self.outputs = outputs
self.comment = comment
self.time = time
self.signatures: List[str] = []
if signing_keys:
self.multi_sign(signing_keys)
def __eq__(self, other: Any) -> bool:
"""
Check Transaction instances equality
"""
if not isinstance(other, Transaction):
return NotImplemented
return (
super().__eq__(other)
and self.signatures == other.signatures
and self.block_id == other.block_id
and self.locktime == other.locktime
and self.issuers == other.issuers
and self.inputs == other.inputs
and self.unlocks == other.unlocks
and self.outputs == other.outputs
and self.comment == other.comment
and self.time == other.time
)
def __hash__(self) -> int:
return hash(
(
self.version,
self.currency,
self.signatures,
self.block_id,
self.locktime,
self.issuers,
self.inputs,
self.unlocks,
self.outputs,
self.comment,
self.time,
)
)
@classmethod
def from_bma_history(
cls: Type[TransactionType], tx_data: Dict, currency: str = G1_CURRENCY_CODENAME
) -> TransactionType:
"""
Get the transaction instance from json
:param tx_data: json data of the transaction
:param currency: Currency codename (default=constants.CURRENCY_CODENAME_G1)
:return:
"""
tx_data = tx_data.copy()
tx_data["currency"] = currency
for data_list in ("issuers", "outputs", "inputs", "unlocks", "signatures"):
tx_data[f"multiline_{data_list}"] = "\n".join(tx_data[data_list])
return cls.from_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
),
tx_data["time"],
)
@classmethod
def from_compact(
cls: Type[TransactionType], compact: str, currency: str = G1_CURRENCY_CODENAME
) -> TransactionType:
"""
Return Transaction instance from compact string format
:param compact: Compact format string
:param currency: Currency codename (default=constants.CURRENCY_CODENAME_G1)
: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
block_id = BlockID.from_str(
Transaction.parse_field("CompactBlockstamp", lines[n])
)
n += 1
issuers = []
inputs = []
unlocks = []
outputs = []
signatures = []
for index in range(0, issuers_num):
issuer = Transaction.parse_field("Pubkey", lines[n + index])
issuers.append(issuer)
n += issuers_num
for index in range(0, inputs_num):
input_source = InputSource.from_inline(lines[n + index])
inputs.append(input_source)
n += inputs_num
for index in range(0, unlocks_num):
unlock = Unlock.from_inline(lines[n + index])
unlocks.append(unlock)
n += unlocks_num
for index in range(0, outputs_num):
output_source = OutputSource.from_inline(lines[n + index])
outputs.append(output_source)
n += outputs_num
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")
transaction = cls(
block_id,
locktime,
issuers,
inputs,
unlocks,
outputs,
comment,
version=version,
currency=currency,
)
# return transaction with signatures
transaction.signatures = signatures
return transaction
@classmethod
def from_signed_raw(
cls: Type[TransactionType], raw: str, time: int = 0
) -> TransactionType:
"""
Return a Transaction instance from a raw string format
:param raw: Raw string format
:param time: time when the transaction enters the blockchain
: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
block_id = BlockID.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(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
transaction = cls(
block_id,
locktime,
issuers,
inputs,
unlocks,
outputs,
comment,
time=time,
version=version,
currency=currency,
)
# return transaction with signatures
transaction.signatures = signatures
return transaction
def raw(self) -> str:
"""
Return raw string format from the instance
:return:
"""
doc = f"Version: {self.version}\n\
Type: Transaction\n\
Currency: {self.currency}\n"
doc += f"Blockstamp: {self.block_id}\n"
doc += f"Locktime: {self.locktime}\n"
doc += "Issuers:\n"
for p in self.issuers:
doc += f"{p}\n"
doc += "Inputs:\n"
for i in self.inputs:
doc += f"{i.inline()}\n"
doc += "Unlocks:\n"
for u in self.unlocks:
doc += f"{u.inline()}\n"
doc += "Outputs:\n"
for o in self.outputs:
doc += f"{o.inline()}\n"
doc += "Comment: "
doc += f"{self.comment}\n"
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:{}:{}:{}:{}:{}:{}:{}\n".format(
self.version,
len(self.issuers),
len(self.inputs),
len(self.unlocks),
len(self.outputs),
"1" if self.comment != "" else "0",
self.locktime,
)
doc += f"{self.block_id}\n"
for pubkey in self.issuers:
doc += f"{pubkey}\n"
for i in self.inputs:
doc += f"{i.inline()}\n"
for u in self.unlocks:
doc += f"{u.inline()}\n"
for o in self.outputs:
doc += f"{o.inline()}\n"
if self.comment != "":
doc += f"{self.comment}\n"
for signature in self.signatures:
doc += f"{signature}\n"
return doc
def signed_raw(self) -> str:
"""
Return signed raw format string
:return:
"""
if not self.signatures:
raise MalformedDocumentError("No signature, can not create raw format")
signed_raw = self.raw()
for signature in self.signatures:
signed_raw += f"{signature}\n"
return signed_raw
def sign(self, key: SigningKey) -> None:
"""
Add a signature to the document from key
:param key: SigningKey instance
:return:
"""
self.multi_sign(key)
def multi_sign(self, keys: Union[SigningKey, List[SigningKey]]) -> None:
"""
Add signature(s) to the document from one key or a list of multiple keys
:param keys: Libnacl key or list of them
"""
if isinstance(keys, SigningKey):
keys = [keys]
for key in keys:
signature = base64.b64encode(key.signature(bytes(self.raw(), "ascii")))
logging.debug("Signature:\n%s", signature.decode("ascii"))
self.signatures.append(signature.decode("ascii"))
def check_signature(self, pubkey: str) -> bool:
"""
Check if document is signed by pubkey
:param pubkey: Base58 pubkey
:return:
"""
return self.check_signatures(pubkey)
def check_signatures(self, pubkeys: Union[str, List[str]]) -> bool:
"""
Check if the signatures matches a public key or a list of public keys
:param pubkeys: Base58 public key or list of them
:return:
"""
if not self.signatures:
raise SignatureException("No signatures, can not check signatures")
if isinstance(pubkeys, str):
pubkeys = [pubkeys]
if len(self.signatures) != len(pubkeys):
raise SignatureException(
"Number of pubkeys not equal to number of signatures"
)
content_to_verify = self.raw()
validation = True
for pubkey, signature in zip(pubkeys, self.signatures):
verifying_key = VerifyingKey(pubkey)
validation = verifying_key.check_signature(content_to_verify, signature)
return validation
class SimpleTransaction(Transaction):
"""
As transaction class, but for only one issuer.
...
"""
def __init__(
self,
block_id: BlockID,
locktime: int,
issuer: str,
single_input: InputSource,
unlocks: List[Unlock],
outputs: List[OutputSource],
comment: str,
time: int = 0,
signing_keys: Optional[Union[SigningKey, List[SigningKey]]] = None,
version: int = VERSION,
currency: str = G1_CURRENCY_CODENAME,
) -> None:
"""
Init instance
:param block_id: BlockID instance
: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 time: time when the transaction enters the blockchain (default=0)
:param signing_keys: SigningKey instance to sign the document (default=None)
:param version: Document version (default=transaction.VERSION)
:param currency: Currency codename (default=constants.CURRENCY_CODENAME_G1)
"""
super().__init__(
block_id,
locktime,
[issuer],
[single_input],
unlocks,
outputs,
comment,
time=time,
signing_keys=signing_keys,
version=version,
currency=currency,
)
@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