798 lines
24 KiB
Python
798 lines
24 KiB
Python
# standard imports
|
||
import logging
|
||
import enum
|
||
import re
|
||
|
||
# external imports
|
||
import coincurve
|
||
import sha3
|
||
from hexathon import (
|
||
strip_0x,
|
||
add_0x,
|
||
compact,
|
||
)
|
||
from rlp import decode as rlp_decode
|
||
from rlp import encode as rlp_encode
|
||
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
|
||
from chainlib.jsonrpc import JSONRPCRequest
|
||
from chainlib.tx import Tx as BaseTx
|
||
from chainlib.eth.nonce import (
|
||
nonce as nonce_query,
|
||
nonce_confirmed as nonce_query_confirmed,
|
||
)
|
||
from chainlib.block import BlockSpec
|
||
|
||
# local imports
|
||
from .address import to_checksum
|
||
from .constant import (
|
||
MINIMUM_FEE_UNITS,
|
||
MINIMUM_FEE_PRICE,
|
||
ZERO_ADDRESS,
|
||
DEFAULT_FEE_LIMIT,
|
||
)
|
||
from .contract import ABIContractEncoder
|
||
from .jsonrpc import to_blockheight_param
|
||
|
||
logg = logging.getLogger(__name__)
|
||
|
||
|
||
|
||
class TxFormat(enum.IntEnum):
|
||
"""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',
|
||
]
|
||
|
||
|
||
count = nonce_query
|
||
count_pending = nonce_query
|
||
count_confirmed = nonce_query_confirmed
|
||
|
||
|
||
def pack(tx_src, chain_spec):
|
||
"""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):
|
||
"""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):
|
||
"""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):
|
||
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')
|
||
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):
|
||
"""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):
|
||
"""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):
|
||
"""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):
|
||
"""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:
|
||
"""Base class for generating and signing transactions or contract calls.
|
||
|
||
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.
|
||
:type param: Object implementing interface ofchainlib.eth.connection.sign_transaction_to_wire
|
||
: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):
|
||
"""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'])
|
||
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):
|
||
"""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):
|
||
"""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):
|
||
"""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()
|
||
gas_price = strip_0x(txes['gasPrice'])
|
||
gas = strip_0x(txes['gas'])
|
||
return {
|
||
'from': tx['from'],
|
||
'to': txes['to'],
|
||
'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):
|
||
"""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)
|
||
elif tx_format == TxFormat.RAW_ARGS:
|
||
return strip_0x(tx['data'])
|
||
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):
|
||
"""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):
|
||
"""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
|
||
|
||
|
||
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))
|
||
height = to_blockheight_param(height)
|
||
o['params'].append(height)
|
||
o = j.finalize(o)
|
||
return o
|
||
|
||
|
||
class Tx(BaseTx):
|
||
"""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
|
||
"""
|
||
|
||
def __init__(self, src, block=None, rcpt=None, strict=False):
|
||
self.__rcpt_block_hash = None
|
||
|
||
src = self.src_normalize(src)
|
||
self.index = -1
|
||
tx_hash = add_0x(src['hash'])
|
||
self.hash = strip_0x(tx_hash)
|
||
if block != None:
|
||
self.apply_block(block)
|
||
try:
|
||
self.value = int(strip_0x(src['value']), 16)
|
||
except TypeError:
|
||
self.value = int(src['value'])
|
||
try:
|
||
self.nonce = int(strip_0x(src['nonce']), 16)
|
||
except TypeError:
|
||
self.nonce = int(src['nonce'])
|
||
address_from = strip_0x(src['from'])
|
||
try:
|
||
self.gas_price = int(strip_0x(src['gasPrice']), 16)
|
||
except TypeError:
|
||
self.gas_price = int(src['gasPrice'])
|
||
try:
|
||
self.gas_limit = int(strip_0x(src['gas']), 16)
|
||
except TypeError:
|
||
self.gas_limit = int(src['gas'])
|
||
self.outputs = [to_checksum(address_from)]
|
||
self.contract = None
|
||
|
||
self.fee_limit = self.gas_limit
|
||
self.fee_price = self.gas_price
|
||
|
||
try:
|
||
inpt = src['input']
|
||
except KeyError:
|
||
inpt = src['data']
|
||
src['input'] = src['data']
|
||
|
||
if inpt != '0x':
|
||
inpt = strip_0x(inpt)
|
||
else:
|
||
inpt = ''
|
||
self.payload = inpt
|
||
|
||
to = src['to']
|
||
if to == None:
|
||
to = ZERO_ADDRESS
|
||
self.inputs = [to_checksum(strip_0x(to))]
|
||
|
||
self.block = block
|
||
try:
|
||
self.wire = src['raw']
|
||
except KeyError:
|
||
logg.debug('no inline raw tx src, and no raw rendering implemented, field will be "None"')
|
||
|
||
self.status = Status.PENDING
|
||
self.logs = None
|
||
|
||
self.tx_rcpt_src = None
|
||
if rcpt != None:
|
||
self.apply_receipt(rcpt, strict=strict)
|
||
|
||
self.v = src.get('v')
|
||
self.r = src.get('r')
|
||
self.s = src.get('s')
|
||
|
||
self.wire = None
|
||
|
||
self.tx_src = src
|
||
|
||
|
||
def src(self):
|
||
"""Retrieve normalized representation source used to construct transaction object.
|
||
|
||
:rtype: dict
|
||
:returns: Transaction representation
|
||
"""
|
||
return self.tx_src
|
||
|
||
|
||
@classmethod
|
||
def src_normalize(self, src):
|
||
"""Normalizes transaction representation source data.
|
||
|
||
:param src: Transaction representation
|
||
:type src: dict
|
||
:rtype: dict
|
||
:returns: Transaction representation, normalized
|
||
"""
|
||
src = snake_and_camel(src)
|
||
|
||
if isinstance(src.get('v'), str):
|
||
try:
|
||
src['v'] = int(src['v'])
|
||
except ValueError:
|
||
src['v'] = int(src['v'], 16)
|
||
return src
|
||
|
||
|
||
def as_dict(self):
|
||
return self.src()
|
||
|
||
|
||
def rcpt_src(self):
|
||
return self.tx_rcpt_src
|
||
|
||
|
||
def apply_receipt(self, rcpt, strict=False):
|
||
"""Apply receipt data to transaction object.
|
||
|
||
Effect is the same as passing a receipt at construction.
|
||
|
||
:param rcpt: Receipt data
|
||
:type rcpt: dict
|
||
"""
|
||
rcpt = self.src_normalize(rcpt)
|
||
logg.debug('rcpt {}'.format(rcpt))
|
||
self.tx_rcpt_src = rcpt
|
||
|
||
tx_hash = add_0x(rcpt['transaction_hash'])
|
||
if rcpt['transaction_hash'] != add_0x(self.hash):
|
||
raise ValueError('rcpt hash {} does not match transaction hash {}'.format(rcpt['transaction_hash'], self.hash))
|
||
|
||
block_hash = add_0x(rcpt['block_hash'])
|
||
if self.block != None:
|
||
if block_hash != add_0x(self.block.hash):
|
||
raise ValueError('rcpt block hash {} does not match transaction block hash {}'.format(rcpt['block_hash'], self.block.hash))
|
||
|
||
try:
|
||
status_number = int(rcpt['status'], 16)
|
||
except TypeError:
|
||
status_number = int(rcpt['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 rcpt['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 = int(rcpt['transaction_index'], 16)
|
||
except TypeError:
|
||
self.tx_index = int(rcpt['transaction_index'])
|
||
# TODO: replace with rpc receipt/transaction translator when available
|
||
contract_address = rcpt.get('contractAddress')
|
||
if contract_address == None:
|
||
contract_address = rcpt.get('contract_address')
|
||
if contract_address != None:
|
||
self.contract = contract_address
|
||
self.logs = rcpt['logs']
|
||
try:
|
||
self.gas_used = int(rcpt['gasUsed'], 16)
|
||
except TypeError:
|
||
self.gas_used = int(rcpt['gasUsed'])
|
||
|
||
self.__rcpt_block_hash = rcpt['block_hash']
|
||
|
||
|
||
def apply_block(self, block):
|
||
"""Apply block to transaction object.
|
||
|
||
:param block: Block object
|
||
:type block: chainlib.block.Block
|
||
"""
|
||
if self.__rcpt_block_hash != None:
|
||
if block.hash != self.__rcpt_block_hash:
|
||
raise ValueError('block hash {} does not match already applied receipt block hash {}'.format(block.hash, self.__rcpt_block_hash))
|
||
self.index = block.get_tx(self.hash)
|
||
self.block = block
|
||
|
||
|
||
def generate_wire(self, chain_spec):
|
||
"""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:
|
||
b = pack(self.src(), chain_spec)
|
||
self.wire = add_0x(b.hex())
|
||
return self.wire
|
||
|
||
|
||
@staticmethod
|
||
def from_src(src, block=None, rcpt=None, strict=False):
|
||
"""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):
|
||
"""Human-readable string dump of transaction contents.
|
||
|
||
:rtype: str
|
||
:returns: Contents
|
||
"""
|
||
s = """hash {}
|
||
from {}
|
||
to {}
|
||
value {}
|
||
nonce {}
|
||
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:
|
||
s += """gas_used {}
|
||
""".format(
|
||
self.gas_used,
|
||
)
|
||
|
||
s += 'status ' + self.status.name + '\n'
|
||
|
||
if self.block != None:
|
||
s += """block_number {}
|
||
block_hash {}
|
||
tx_index {}
|
||
""".format(
|
||
self.block.number,
|
||
self.block.hash,
|
||
self.tx_index,
|
||
)
|
||
|
||
|
||
if self.contract != None:
|
||
s += """contract {}
|
||
""".format(
|
||
self.contract,
|
||
)
|
||
|
||
if self.wire != None:
|
||
s += """src {}
|
||
""".format(
|
||
self.wire,
|
||
)
|
||
|
||
return s
|