chainlib-eth/chainlib/eth/tx.py

784 lines
23 KiB
Python
Raw Normal View History

# standard imports
import logging
import enum
import re
# external imports
import coincurve
import sha3
from hexathon import (
strip_0x,
add_0x,
2021-08-21 09:27:40 +02:00
compact,
2022-05-09 12:00:29 +02:00
to_int as hex_to_int,
same as hex_same,
)
from rlp import decode as rlp_decode
from rlp import encode as rlp_encode
2021-10-18 14:23:54 +02:00
from funga.eth.transaction import EIP155Transaction
from funga.eth.encoding import (
public_key_to_address,
chain_id_to_v,
)
from potaahto.symbols import snake_and_camel
from chainlib.hash import keccak256_hex_to_hex
from chainlib.status import Status
2021-08-21 09:27:40 +02:00
from chainlib.jsonrpc import JSONRPCRequest
2022-05-09 12:00:29 +02:00
from chainlib.tx import (
Tx as BaseTx,
TxResult as BaseTxResult,
)
2021-08-21 09:27:40 +02:00
from chainlib.eth.nonce import (
nonce as nonce_query,
nonce_confirmed as nonce_query_confirmed,
)
from chainlib.eth.address import is_same_address
2021-08-21 09:27:40 +02:00
from chainlib.block import BlockSpec
2022-05-09 12:00:29 +02:00
from chainlib.src import SrcItem
2021-08-21 09:27:40 +02:00
# local imports
from .address import to_checksum
from .constant import (
MINIMUM_FEE_UNITS,
MINIMUM_FEE_PRICE,
ZERO_ADDRESS,
2021-08-21 09:27:40 +02:00
DEFAULT_FEE_LIMIT,
)
from .contract import ABIContractEncoder
2021-08-21 09:27:40 +02:00
from .jsonrpc import to_blockheight_param
2022-05-09 12:00:29 +02:00
from .src import Src
2021-08-21 09:27:40 +02:00
logg = logging.getLogger(__name__)
class TxFormat(enum.IntEnum):
2021-08-21 09:27:40 +02:00
"""Tx generator output formats
"""
DICT = 0x00
RAW = 0x01
RAW_SIGNED = 0x02
RAW_ARGS = 0x03
RLP = 0x10
RLP_SIGNED = 0x11
JSONRPC = 0x10
field_debugs = [
'nonce',
'gasPrice',
'gas',
'to',
'value',
'data',
'v',
'r',
's',
]
2021-08-21 09:27:40 +02:00
count = nonce_query
count_pending = nonce_query
count_confirmed = nonce_query_confirmed
def pack(tx_src, chain_spec):
2021-08-21 09:27:40 +02:00
"""Serialize wire format transaction from transaction representation.
:param tx_src: Transaction source.
:type tx_src: dict
:param chain_spec: Chain spec to calculate EIP155 v value
:type chain_spec: chainlib.chain.ChainSpec
:rtype: bytes
:returns: Serialized transaction
"""
if isinstance(tx_src, Tx):
tx_src = tx_src.as_dict()
tx_src = Tx.src_normalize(tx_src)
tx = EIP155Transaction(tx_src, tx_src['nonce'], chain_spec.chain_id())
signature = bytearray(65)
cursor = 0
for a in [
tx_src['r'],
tx_src['s'],
]:
try:
a = strip_0x(a)
except TypeError:
a = strip_0x(hex(a)) # believe it or not, eth_tester returns signatures as ints not hex
for b in bytes.fromhex(a):
signature[cursor] = b
cursor += 1
#signature[cursor] = chainv_to_v(chain_spec.chain_id(), tx_src['v'])
tx.apply_signature(chain_spec.chain_id(), signature, v=tx_src['v'])
logg.debug('tx {}'.format(tx.serialize()))
return tx.rlp_serialize()
def unpack(tx_raw_bytes, chain_spec):
2021-08-21 09:27:40 +02:00
"""Deserialize wire format transaction to transaction representation.
:param tx_raw_bytes: Serialized transaction
:type tx_raw_bytes: bytes
:param chain_spec: Chain spec to calculate EIP155 v value
:type chain_spec: chainlib.chain.ChainSpec
:rtype: dict
:returns: Transaction representation
"""
chain_id = chain_spec.chain_id()
tx = __unpack_raw(tx_raw_bytes, chain_id)
tx['nonce'] = int.from_bytes(tx['nonce'], 'big')
tx['gasPrice'] = int.from_bytes(tx['gasPrice'], 'big')
tx['gas'] = int.from_bytes(tx['gas'], 'big')
tx['value'] = int.from_bytes(tx['value'], 'big')
return tx
def unpack_hex(tx_raw_bytes, chain_spec):
2021-08-21 09:27:40 +02:00
"""Deserialize wire format transaction to transaction representation, using hex values for all numeric value fields.
:param tx_raw_bytes: Serialized transaction
:type tx_raw_bytes: bytes
:param chain_spec: Chain spec to calculate EIP155 v value
:type chain_spec: chainlib.chain.ChainSpec
:rtype: dict
:returns: Transaction representation
"""
chain_id = chain_spec.chain_id()
tx = __unpack_raw(tx_raw_bytes, chain_id)
tx['nonce'] = add_0x(hex(tx['nonce']))
tx['gasPrice'] = add_0x(hex(tx['gasPrice']))
tx['gas'] = add_0x(hex(tx['gas']))
tx['value'] = add_0x(hex(tx['value']))
tx['chainId'] = add_0x(hex(tx['chainId']))
return tx
def __unpack_raw(tx_raw_bytes, chain_id=1):
2021-06-28 09:10:53 +02:00
try:
d = rlp_decode(tx_raw_bytes)
except Exception as e:
raise ValueError('RLP deserialization failed: {}'.format(e))
logg.debug('decoding using chain id {}'.format(str(chain_id)))
j = 0
for i in d:
v = i.hex()
if j != 3 and v == '':
v = '00'
logg.debug('decoded {}: {}'.format(field_debugs[j], v))
j += 1
vb = chain_id
if chain_id != 0:
v = int.from_bytes(d[6], 'big')
2022-01-23 21:36:29 +01:00
if v > 29:
vb = v - (chain_id * 2) - 35
r = bytearray(32)
r[32-len(d[7]):] = d[7]
s = bytearray(32)
s[32-len(d[8]):] = d[8]
logg.debug('vb {}'.format(vb))
sig = b''.join([r, s, bytes([vb])])
h = sha3.keccak_256()
h.update(rlp_encode(d))
signed_hash = h.digest()
d[6] = chain_id
d[7] = b''
d[8] = b''
h = sha3.keccak_256()
h.update(rlp_encode(d))
unsigned_hash = h.digest()
#p = so.recover_public_key_from_msg_hash(unsigned_hash)
#a = p.to_checksum_address()
pubk = coincurve.PublicKey.from_signature_and_message(sig, unsigned_hash, hasher=None)
a = public_key_to_address(pubk)
logg.debug('decoded recovery byte {}'.format(vb))
logg.debug('decoded address {}'.format(a))
logg.debug('decoded signed hash {}'.format(signed_hash.hex()))
logg.debug('decoded unsigned hash {}'.format(unsigned_hash.hex()))
to = d[3].hex() or None
if to != None:
to = to_checksum(to)
data = d[5].hex()
try:
data = add_0x(data)
except:
data = '0x'
return {
'from': a,
'to': to,
'nonce': d[0],
'gasPrice': d[1],
'gas': d[2],
'value': d[4],
'data': data,
'v': v,
'recovery_byte': vb,
'r': add_0x(sig[:32].hex()),
's': add_0x(sig[32:64].hex()),
'chainId': chain_id,
'hash': add_0x(signed_hash.hex()),
'hash_unsigned': add_0x(unsigned_hash.hex()),
}
def transaction(hsh, id_generator=None):
2021-08-21 09:27:40 +02:00
"""Generate json-rpc query to retrieve transaction by hash from node.
:param hsh: Transaction hash, in hex
:type hsh: str
:param id_generator: json-rpc id generator
:type id_generator: JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'eth_getTransactionByHash'
o['params'].append(add_0x(hsh))
return j.finalize(o)
def transaction_by_block(hsh, idx, id_generator=None):
2021-08-21 09:27:40 +02:00
"""Generate json-rpc query to retrieve transaction by block hash and index.
:param hsh: Block hash, in hex
:type hsh: str
:param idx: Transaction index
:type idx: int
:param id_generator: json-rpc id generator
:type id_generator: JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'eth_getTransactionByBlockHashAndIndex'
o['params'].append(add_0x(hsh))
o['params'].append(hex(idx))
return j.finalize(o)
def receipt(hsh, id_generator=None):
2021-08-21 09:27:40 +02:00
"""Generate json-rpc query to retrieve transaction receipt by transaction hash from node.
:param hsh: Transaction hash, in hex
:type hsh: str
:param id_generator: json-rpc id generator
:type id_generator: JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'eth_getTransactionReceipt'
o['params'].append(add_0x(hsh))
return j.finalize(o)
def raw(tx_raw_hex, id_generator=None):
2021-08-21 09:27:40 +02:00
"""Generator json-rpc query to send raw transaction to node.
:param hsh: Serialized transaction, in hex
:type hsh: str
:param id_generator: json-rpc id generator
:type id_generator: JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'eth_sendRawTransaction'
o['params'].append(add_0x(tx_raw_hex))
return j.finalize(o)
class TxFactory:
2021-08-21 09:27:40 +02:00
"""Base class for generating and signing transactions or contract calls.
2021-08-21 09:27:40 +02:00
For transactions (state changes), a signer, gas oracle and nonce oracle needs to be supplied.
Gas oracle and nonce oracle may in some cases be needed for contract calls, if the node insists on counting gas for read-only operations.
:param chain_spec: Chain spec to use for signer.
:type chain_spec: chainlib.chain.ChainSpec
:param signer: Signer middleware.
2021-08-24 17:55:01 +02:00
:type param: Object implementing interface ofchainlib.eth.connection.sign_transaction_to_wire
2021-08-21 09:27:40 +02:00
:param gas_oracle: Backend to generate gas parameters
:type gas_oracle: Object implementing chainlib.eth.gas.GasOracle interface
:param nonce_oracle: Backend to generate gas parameters
:type nonce_oracle: Object implementing chainlib.eth.nonce.NonceOracle interface
"""
fee = DEFAULT_FEE_LIMIT
def __init__(self, chain_spec, signer=None, gas_oracle=None, nonce_oracle=None):
self.gas_oracle = gas_oracle
self.nonce_oracle = nonce_oracle
self.chain_spec = chain_spec
self.signer = signer
def build_raw(self, tx):
2021-08-21 09:27:40 +02:00
"""Sign transaction data, returning the transaction hash and serialized transaction.
In most cases, chainlib.eth.tx.TxFactory.finalize should be used instead.
:param tx: Transaction representation
:type tx: dict
:rtype: tuple
:returns: Transaction hash (in hex), serialized transaction (in hex)
"""
if tx['to'] == None or tx['to'] == '':
tx['to'] = '0x'
txe = EIP155Transaction(tx, tx['nonce'], tx['chainId'])
2021-08-24 17:55:01 +02:00
tx_raw = self.signer.sign_transaction_to_wire(txe)
tx_raw_hex = add_0x(tx_raw.hex())
tx_hash_hex = add_0x(keccak256_hex_to_hex(tx_raw_hex))
return (tx_hash_hex, tx_raw_hex)
def build(self, tx, id_generator=None):
2021-08-21 09:27:40 +02:00
"""Sign transaction and wrap in raw transaction json-rpc query.
In most cases, chainlib.eth.tx.TxFactory.finalize should be used instead.
:param tx: Transaction representation
type tx: dict
:param id_generator: JSONRPC id generator
:type id_generator: JSONRPCIdGenerator
:rtype: tuple
:returns: Transaction hash (in hex), raw transaction rpc query object
"""
(tx_hash_hex, tx_raw_hex) = self.build_raw(tx)
o = raw(tx_raw_hex, id_generator=id_generator)
return (tx_hash_hex, o)
def template(self, sender, recipient, use_nonce=False):
2021-08-21 09:27:40 +02:00
"""Generate a base transaction template.
:param sender: Sender address, in hex
:type sender: str
:param receipient: Recipient address, in hex
:type recipient: str
:param use_nonce: Use and advance nonce in nonce generator.
:type use_nonce: bool
:rtype: dict
:returns: Transaction representation.
"""
gas_price = MINIMUM_FEE_PRICE
gas_limit = MINIMUM_FEE_UNITS
if self.gas_oracle != None:
(gas_price, gas_limit) = self.gas_oracle.get_gas()
logg.debug('using gas price {} limit {}'.format(gas_price, gas_limit))
nonce = 0
o = {
'from': sender,
'to': recipient,
'value': 0,
'data': '0x',
'gasPrice': gas_price,
'gas': gas_limit,
'chainId': self.chain_spec.chain_id(),
}
if self.nonce_oracle != None and use_nonce:
nonce = self.nonce_oracle.next_nonce()
logg.debug('using nonce {} for address {}'.format(nonce, sender))
o['nonce'] = nonce
return o
def normalize(self, tx):
2021-08-21 09:27:40 +02:00
"""Generate field name redundancies (camel-case, snake-case).
:param tx: Transaction representation
:type tx: dict
:rtype: dict:
:returns: Transaction representation with redudant field names
"""
txe = EIP155Transaction(tx, tx['nonce'], tx['chainId'])
txes = txe.serialize()
2022-02-22 15:47:06 +01:00
gas_price = strip_0x(txes['gasPrice'])
gas = strip_0x(txes['gas'])
return {
'from': tx['from'],
'to': txes['to'],
2022-02-22 15:47:06 +01:00
'gasPrice': add_0x(compact(gas_price)),
'gas': add_0x(compact(gas)),
'data': txes['data'],
}
def finalize(self, tx, tx_format=TxFormat.JSONRPC, id_generator=None):
2021-08-21 09:27:40 +02:00
"""Sign transaction and for specified output format.
:param tx: Transaction representation
:type tx: dict
:param tx_format: Transaction output format
:type tx_format: chainlib.eth.tx.TxFormat
:raises NotImplementedError: Unknown tx_format value
:rtype: varies
:returns: Transaction output in specified format.
"""
if tx_format == TxFormat.JSONRPC:
return self.build(tx, id_generator=id_generator)
elif tx_format == TxFormat.RLP_SIGNED:
return self.build_raw(tx)
2021-12-06 19:00:55 +01:00
elif tx_format == TxFormat.RAW_ARGS:
return strip_0x(tx['data'])
2022-04-10 21:04:50 +02:00
elif tx_format == TxFormat.DICT:
return tx
raise NotImplementedError('tx formatting {} not implemented'.format(tx_format))
def set_code(self, tx, data, update_fee=True):
2021-08-21 09:27:40 +02:00
"""Apply input data to transaction.
:param tx: Transaction representation
:type tx: dict
:param data: Input data to apply, in hex
:type data: str
:param update_fee: Recalculate gas limit based on added input
:type update_fee: bool
:rtype: dict
:returns: Transaction representation
"""
tx['data'] = data
if update_fee:
tx['gas'] = TxFactory.fee
if self.gas_oracle != None:
(price, tx['gas']) = self.gas_oracle.get_gas(code=data)
else:
logg.debug('using hardcoded gas limit of 8000000 until we have reliable vm executor')
return tx
def transact_noarg(self, method, contract_address, sender_address, tx_format=TxFormat.JSONRPC):
2021-08-21 09:27:40 +02:00
"""Convenience generator for contract transaction with no arguments.
:param method: Method name
:type method: str
:param contract_address: Contract address to transaction against, in hex
:type contract_address: str
:param sender_address: Transaction sender, in hex
:type sender_address: str
:param tx_format: Transaction output format
:type tx_format: chainlib.eth.tx.TxFormat
:rtype: varies
:returns: Transaction output in selected format
"""
enc = ABIContractEncoder()
enc.method(method)
data = enc.get()
tx = self.template(sender_address, contract_address, use_nonce=True)
tx = self.set_code(tx, data)
tx = self.finalize(tx, tx_format)
return tx
2021-08-21 09:27:40 +02:00
def call_noarg(self, method, contract_address, sender_address=ZERO_ADDRESS, height=BlockSpec.LATEST, id_generator=None):
"""Convenience generator for contract (read-only) call with no arguments.
:param method: Method name
:type method: str
:param contract_address: Contract address to transaction against, in hex
:type contract_address: str
:param sender_address: Transaction sender, in hex
:type sender_address: str
:param height: Transaction height specifier
:type height: chainlib.block.BlockSpec
:param id_generator: json-rpc id generator
:type id_generator: JSONRPCIdGenerator
:rtype: varies
:returns: Transaction output in selected format
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
enc = ABIContractEncoder()
enc.method(method)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
2021-08-21 09:27:40 +02:00
height = to_blockheight_param(height)
o['params'].append(height)
o = j.finalize(o)
return o
class TxResult(BaseTxResult, Src):
def apply_src(self, v):
self.contract = None
2022-05-10 21:02:49 +02:00
v = super(TxResult, self).apply_src(v)
self.set_hash(v['transaction_hash'])
try:
status_number = int(v['status'], 16)
except TypeError:
status_number = int(v['status'])
except KeyError as e:
if strict:
raise(e)
logg.debug('setting "success" status on missing status property for {}'.format(self.hash))
status_number = 1
if v['block_number'] == None:
self.status = Status.PENDING
else:
if status_number == 1:
self.status = Status.SUCCESS
elif status_number == 0:
self.status = Status.ERROR
try:
self.tx_index = hex_to_int(v['transaction_index'])
except TypeError:
self.tx_index = int(v['transaction_index'])
self.block_hash = v['block_hash']
# TODO: replace with rpc receipt/transaction translator when available
contract_address = v.get('contract_address')
if contract_address != None:
self.contract = contract_address
self.logs = v['logs']
try:
self.fee_cost = hex_to_int(v['gas_used'])
except TypeError:
self.fee_cost = int(v['gas_used'])
2022-05-09 12:00:29 +02:00
class Tx(BaseTx, Src):
2021-08-21 09:27:40 +02:00
"""Wraps transaction data, transaction receipt data and block data, enforces local standardization of fields, and provides useful output formats for viewing transaction contents.
If block is applied, the transaction data or transaction hash must exist in its transactions array.
If receipt is applied, the transaction hash in the receipt must match the hash in the transaction data.
:param src: Transaction representation
:type src: dict
:param block: Apply block object in which transaction in mined.
:type block: chainlib.block.Block
:param rcpt: Apply receipt data
:type rcpt: dict
#:todo: force tx type schema parser (whether expect hex or int etc)
#:todo: divide up constructor method
"""
2022-05-09 12:00:29 +02:00
def __init__(self, src, block=None, result=None, strict=False, rcpt=None):
# backwards compat
self.gas_price = None
self.gas_limit = None
self.contract = None
self.v = None
self.r = None
self.s = None
super(Tx, self).__init__(src, block=block, result=result, strict=strict)
if result == None and rcpt != None:
self.apply_receipt(rcpt)
2022-05-09 12:00:29 +02:00
def apply_src(self, src):
try:
inpt = src['input']
except KeyError:
inpt = src['data']
src['input'] = src['data']
src = super(Tx, self).apply_src(src)
hsh = self.normal(src['hash'], SrcItem.HASH)
self.set_hash(hsh)
2021-08-21 09:27:40 +02:00
try:
2022-05-09 12:00:29 +02:00
self.value = hex_to_int(src['value'])
except TypeError:
self.value = int(src['value'])
2022-05-09 12:00:29 +02:00
try:
2022-05-09 12:00:29 +02:00
self.nonce = hex_to_int(src['nonce'])
except TypeError:
self.nonce = int(src['nonce'])
2022-05-09 12:00:29 +02:00
try:
2022-05-09 12:00:29 +02:00
self.fee_limit = hex_to_int(src['gas'])
except TypeError:
2022-05-09 12:00:29 +02:00
self.fee_limit = int(src['gas'])
try:
2022-05-09 12:00:29 +02:00
self.fee_price = hex_to_int(src['gas_price'])
except TypeError:
2022-05-09 12:00:29 +02:00
self.fee_price = int(src['gas_price'])
2022-05-09 12:00:29 +02:00
self.gas_price = self.fee_price
self.gas_limit = self.fee_limit
2022-04-19 21:46:11 +02:00
2022-05-09 12:00:29 +02:00
address_from = self.normal(src['from'], SrcItem.ADDRESS)
self.outputs = [to_checksum(address_from)]
to = src['to']
if to == None:
to = ZERO_ADDRESS
self.inputs = [to_checksum(strip_0x(to))]
2022-05-09 12:00:29 +02:00
self.payload = self.normal(src['input'], SrcItem.PAYLOAD)
try:
2022-05-09 12:00:29 +02:00
self.set_wire(src['raw'])
except KeyError:
logg.debug('no inline raw tx src, and no raw rendering implemented, field will be "None"')
self.v = src.get('v')
self.r = src.get('r')
self.s = src.get('s')
2022-05-09 21:21:45 +02:00
#self.status = Status.PENDING
2021-08-21 09:27:40 +02:00
def as_dict(self):
2022-05-09 21:21:45 +02:00
return self.src
def apply_receipt(self, rcpt, strict=False):
result = TxResult(rcpt)
self.apply_result(result)
def apply_result(self, result, strict=False):
2021-08-21 09:27:40 +02:00
"""Apply receipt data to transaction object.
Effect is the same as passing a receipt at construction.
:param rcpt: Receipt data
:type rcpt: dict
"""
if not hex_same(result.hash, self.hash):
raise ValueError('result hash {} does not match transaction hash {}'.format(result.hash, self.hash))
2021-08-21 09:27:40 +02:00
if self.block != None:
if not hex_same(result.block_hash, self.block.hash):
raise ValueError('result block hash {} does not match transaction block hash {}'.format(result.block_hash, self.block.hash))
2021-08-21 09:27:40 +02:00
super(Tx, self).apply_result(result)
2021-08-21 09:27:40 +02:00
def apply_block(self, block):
2021-08-21 09:27:40 +02:00
"""Apply block to transaction object.
:param block: Block object
:type block: chainlib.block.Block
"""
self.index = block.get_tx(self.hash)
self.block = block
def generate_wire(self, chain_spec):
2021-08-21 09:27:40 +02:00
"""Generate transaction wire format.
:param chain_spec: Chain spec to interpret EIP155 v value.
:type chain_spec: chainlib.chain.ChainSpec
:rtype: str
:returns: Wire format, in hex
"""
if self.wire == None:
2022-05-10 21:02:49 +02:00
b = pack(self.src, chain_spec)
self.set_wire(add_0x(b.hex()))
2021-08-21 09:27:40 +02:00
return self.wire
@staticmethod
def from_src(src, block=None, rcpt=None, strict=False):
2021-08-21 09:27:40 +02:00
"""Creates a new Tx object.
Alias of constructor.
"""
return Tx(src, block=block, rcpt=rcpt, strict=strict)
def __str__(self):
if self.block != None:
return 'tx {} status {} block {} index {}'.format(add_0x(self.hash), self.status.name, self.block.number, self.index)
else:
return 'tx {} status {}'.format(add_0x(self.hash), self.status.name)
def __repr__(self):
return self.__str__()
def to_human(self):
2021-08-21 09:27:40 +02:00
"""Human-readable string dump of transaction contents.
:rtype: str
:returns: Contents
"""
s = """hash {}
from {}
to {}
value {}
nonce {}
2021-08-21 09:27:40 +02:00
gas_price {}
gas_limit {}
input {}
""".format(
self.hash,
self.outputs[0],
self.inputs[0],
self.value,
self.nonce,
self.gas_price,
self.gas_limit,
self.payload,
)
if self.status != Status.PENDING:
2021-08-21 09:27:40 +02:00
s += """gas_used {}
""".format(
2022-05-10 21:02:49 +02:00
self.result.fee_cost,
)
s += 'status ' + self.status.name + '\n'
2021-08-21 09:27:40 +02:00
if self.block != None:
s += """block_number {}
block_hash {}
tx_index {}
""".format(
self.block.number,
self.block.hash,
2022-05-10 21:02:49 +02:00
self.result.tx_index,
2021-08-21 09:27:40 +02:00
)
if self.contract != None:
s += """contract {}
""".format(
self.contract,
)
if self.wire != None:
s += """src {}
""".format(
2022-05-10 21:02:49 +02:00
str(self.wire),
)
return s