chainlib/chainlib/eth/tx.py

443 lines
12 KiB
Python
Raw Normal View History

2021-02-08 11:23:07 +01:00
# standard imports
import logging
2021-04-04 14:55:27 +02:00
import enum
2021-04-08 17:39:32 +02:00
import re
2021-02-08 11:23:07 +01:00
2021-04-04 14:55:27 +02:00
# external imports
import coincurve
2021-02-08 11:23:07 +01:00
import sha3
2021-02-12 00:24:10 +01:00
from hexathon import (
strip_0x,
add_0x,
)
2021-02-08 11:23:07 +01:00
from rlp import decode as rlp_decode
from rlp import encode as rlp_encode
2021-02-09 12:12:37 +01:00
from crypto_dev_signer.eth.transaction import EIP155Transaction
2021-04-04 14:55:27 +02:00
from crypto_dev_signer.encoding import public_key_to_address
2021-04-14 14:45:37 +02:00
from potaahto.symbols import snake_and_camel
2021-02-08 11:23:07 +01:00
2021-02-12 00:24:10 +01:00
2021-02-08 11:23:07 +01:00
# local imports
2021-02-12 00:24:10 +01:00
from chainlib.hash import keccak256_hex_to_hex
from chainlib.status import Status
2021-02-08 11:23:07 +01:00
from .address import to_checksum
2021-02-09 12:12:37 +01:00
from .constant import (
MINIMUM_FEE_UNITS,
MINIMUM_FEE_PRICE,
2021-02-11 12:43:54 +01:00
ZERO_ADDRESS,
2021-02-09 12:12:37 +01:00
)
from .contract import ABIContractEncoder
2021-04-04 14:55:27 +02:00
from chainlib.jsonrpc import jsonrpc_template
2021-02-08 11:23:07 +01:00
2021-04-04 14:55:27 +02:00
logg = logging.getLogger().getChild(__name__)
2021-02-08 11:23:07 +01:00
2021-04-14 14:45:37 +02:00
2021-04-04 14:55:27 +02:00
class TxFormat(enum.IntEnum):
DICT = 0x00
RAW = 0x01
RAW_SIGNED = 0x02
RAW_ARGS = 0x03
RLP = 0x10
RLP_SIGNED = 0x11
JSONRPC = 0x10
2021-02-08 11:23:07 +01:00
field_debugs = [
'nonce',
'gasPrice',
'gas',
'to',
'value',
'data',
'v',
'r',
's',
]
2021-04-04 14:55:27 +02:00
def count(address, confirmed=False):
o = jsonrpc_template()
o['method'] = 'eth_getTransactionCount'
o['params'].append(address)
if confirmed:
o['params'].append('latest')
else:
o['params'].append('pending')
return o
count_pending = count
def count_confirmed(address):
return count(address, True)
def unpack(tx_raw_bytes, chain_spec):
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):
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-02-08 11:23:07 +01:00
d = rlp_decode(tx_raw_bytes)
2021-04-04 14:55:27 +02:00
logg.debug('decoding using chain id {}'.format(str(chain_id)))
2021-02-08 11:23:07 +01:00
j = 0
for i in d:
2021-04-04 14:55:27 +02:00
v = i.hex()
if j != 3 and v == '':
v = '00'
logg.debug('decoded {}: {}'.format(field_debugs[j], v))
2021-02-08 11:23:07 +01:00
j += 1
vb = chain_id
if chain_id != 0:
v = int.from_bytes(d[6], 'big')
vb = v - (chain_id * 2) - 35
2021-04-04 14:55:27 +02:00
r = bytearray(32)
r[32-len(d[7]):] = d[7]
s = bytearray(32)
s[32-len(d[8]):] = d[8]
sig = b''.join([r, s, bytes([vb])])
#so = KeyAPI.Signature(signature_bytes=sig)
2021-02-08 11:23:07 +01:00
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()
2021-04-04 14:55:27 +02:00
#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)
2021-02-08 11:23:07 +01:00
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)
2021-04-04 14:55:27 +02:00
data = d[5].hex()
try:
data = add_0x(data)
except:
data = '0x'
2021-02-08 11:23:07 +01:00
return {
'from': a,
'to': to,
2021-04-04 14:55:27 +02:00
'nonce': d[0],
'gasPrice': d[1],
'gas': d[2],
'value': d[4],
'data': data,
'v': chain_id,
2021-04-04 14:55:27 +02:00
'r': add_0x(sig[:32].hex()),
's': add_0x(sig[32:64].hex()),
'chainId': chain_id,
2021-04-04 14:55:27 +02:00
'hash': add_0x(signed_hash.hex()),
'hash_unsigned': add_0x(unsigned_hash.hex()),
2021-02-08 11:23:07 +01:00
}
2021-04-04 14:55:27 +02:00
def transaction(hsh):
o = jsonrpc_template()
o['method'] = 'eth_getTransactionByHash'
o['params'].append(add_0x(hsh))
return o
2021-04-29 08:35:53 +02:00
2021-04-04 14:55:27 +02:00
def transaction_by_block(hsh, idx):
o = jsonrpc_template()
o['method'] = 'eth_getTransactionByBlockHashAndIndex'
o['params'].append(add_0x(hsh))
o['params'].append(hex(idx))
return o
2021-02-17 10:13:22 +01:00
def receipt(hsh):
o = jsonrpc_template()
o['method'] = 'eth_getTransactionReceipt'
o['params'].append(add_0x(hsh))
2021-02-17 10:13:22 +01:00
return o
2021-04-04 14:55:27 +02:00
def raw(tx_raw_hex):
o = jsonrpc_template()
o['method'] = 'eth_sendRawTransaction'
2021-05-19 09:55:32 +02:00
o['params'].append(add_0x(tx_raw_hex))
2021-04-04 14:55:27 +02:00
return o
2021-02-09 09:42:46 +01:00
class TxFactory:
2021-04-04 14:55:27 +02:00
fee = 8000000
def __init__(self, chain_spec, signer=None, gas_oracle=None, nonce_oracle=None):
2021-02-09 09:42:46 +01:00
self.gas_oracle = gas_oracle
self.nonce_oracle = nonce_oracle
2021-04-04 14:55:27 +02:00
self.chain_spec = chain_spec
2021-02-09 09:42:46 +01:00
self.signer = signer
2021-02-17 10:13:22 +01:00
def build_raw(self, tx):
if tx['to'] == None or tx['to'] == '':
tx['to'] = '0x'
2021-02-12 00:24:10 +01:00
txe = EIP155Transaction(tx, tx['nonce'], tx['chainId'])
2021-04-04 14:55:27 +02:00
tx_raw = self.signer.sign_transaction_to_rlp(txe)
2021-02-12 00:24:10 +01:00
tx_raw_hex = add_0x(tx_raw.hex())
tx_hash_hex = add_0x(keccak256_hex_to_hex(tx_raw_hex))
2021-02-17 10:13:22 +01:00
return (tx_hash_hex, tx_raw_hex)
2021-04-04 14:55:27 +02:00
2021-02-17 10:13:22 +01:00
def build(self, tx):
(tx_hash_hex, tx_raw_hex) = self.build_raw(tx)
2021-04-04 14:55:27 +02:00
o = raw(tx_raw_hex)
2021-02-12 00:24:10 +01:00
return (tx_hash_hex, o)
def template(self, sender, recipient, use_nonce=False):
2021-02-09 12:12:37 +01:00
gas_price = MINIMUM_FEE_PRICE
gas_limit = MINIMUM_FEE_UNITS
2021-02-09 12:12:37 +01:00
if self.gas_oracle != None:
2021-04-04 14:55:27 +02:00
(gas_price, gas_limit) = self.gas_oracle.get_gas()
logg.debug('using gas price {} limit {}'.format(gas_price, gas_limit))
2021-02-09 12:12:37 +01:00
nonce = 0
o = {
2021-02-09 09:42:46 +01:00
'from': sender,
'to': recipient,
'value': 0,
'data': '0x',
'gasPrice': gas_price,
'gas': gas_limit,
2021-04-04 14:55:27 +02:00
'chainId': self.chain_spec.chain_id(),
2021-02-09 09:42:46 +01:00
}
if self.nonce_oracle != None and use_nonce:
2021-04-04 14:55:27 +02:00
nonce = self.nonce_oracle.next_nonce()
logg.debug('using nonce {} for address {}'.format(nonce, sender))
o['nonce'] = nonce
return o
2021-02-09 12:12:37 +01:00
def normalize(self, tx):
txe = EIP155Transaction(tx, tx['nonce'], tx['chainId'])
txes = txe.serialize()
return {
'from': tx['from'],
'to': txes['to'],
'gasPrice': txes['gasPrice'],
'gas': txes['gas'],
'data': txes['data'],
}
2021-04-04 14:55:27 +02:00
def finalize(self, tx, tx_format=TxFormat.JSONRPC):
if tx_format == TxFormat.JSONRPC:
return self.build(tx)
elif tx_format == TxFormat.RLP_SIGNED:
return self.build_raw(tx)
raise NotImplementedError('tx formatting {} not implemented'.format(tx_format))
2021-02-09 12:12:37 +01:00
def set_code(self, tx, data, update_fee=True):
tx['data'] = data
if update_fee:
2021-04-04 14:55:27 +02:00
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')
2021-02-09 12:12:37 +01:00
return tx
2021-02-11 10:16:05 +01:00
def transact_noarg(self, method, contract_address, sender_address, tx_format=TxFormat.JSONRPC):
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):
o = jsonrpc_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))
o['params'].append('latest')
return o
2021-02-11 10:16:05 +01:00
class Tx:
2021-04-08 17:39:32 +02:00
# TODO: force tx type schema parser (whether expect hex or int etc)
def __init__(self, src, block=None, rcpt=None):
2021-04-08 17:39:32 +02:00
logg.debug('src {}'.format(src))
self.src = self.src_normalize(src)
2021-02-12 00:24:10 +01:00
self.index = -1
2021-04-08 17:39:32 +02:00
tx_hash = add_0x(src['hash'])
2021-02-12 00:24:10 +01:00
if block != None:
2021-04-08 17:39:32 +02:00
i = 0
for tx in block.txs:
tx_hash_block = None
try:
tx_hash_block = tx['hash']
except TypeError:
tx_hash_block = add_0x(tx)
logg.debug('tx {} cmp {}'.format(tx, tx_hash))
if tx_hash_block == tx_hash:
self.index = i
break
i += 1
if self.index == -1:
raise AttributeError('tx {} not found in block {}'.format(tx_hash, block.hash))
self.block = block
self.hash = strip_0x(tx_hash)
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'])
2021-02-12 00:24:10 +01:00
address_from = strip_0x(src['from'])
2021-04-08 17:39:32 +02:00
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'])
2021-02-12 00:24:10 +01:00
self.outputs = [to_checksum(address_from)]
2021-04-04 14:55:27 +02:00
self.contract = None
2021-02-11 12:43:54 +01:00
2021-04-08 17:39:32 +02:00
try:
inpt = src['input']
except KeyError:
inpt = src['data']
2021-02-11 12:43:54 +01:00
if inpt != '0x':
inpt = strip_0x(inpt)
else:
2021-04-14 14:45:37 +02:00
inpt = ''
2021-02-11 12:43:54 +01:00
self.payload = inpt
to = src['to']
if to == None:
to = ZERO_ADDRESS
2021-02-12 00:24:10 +01:00
self.inputs = [to_checksum(strip_0x(to))]
2021-02-11 12:43:54 +01:00
2021-02-11 10:16:05 +01:00
self.block = block
2021-04-04 14:55:27 +02:00
try:
self.wire = src['raw']
except KeyError:
logg.warning('no inline raw tx src, and no raw rendering implemented, field will be "None"')
2021-02-11 12:43:54 +01:00
self.src = src
2021-02-11 10:16:05 +01:00
self.status = Status.PENDING
self.logs = None
if rcpt != None:
self.apply_receipt(rcpt)
2021-04-08 17:39:32 +02:00
@classmethod
def src_normalize(self, src):
2021-04-14 14:45:37 +02:00
return snake_and_camel(src)
2021-04-15 17:51:39 +02:00
def apply_receipt(self, rcpt):
2021-05-27 14:25:48 +02:00
rcpt = self.src_normalize(rcpt)
2021-04-08 17:39:32 +02:00
logg.debug('rcpt {}'.format(rcpt))
2021-04-14 14:45:37 +02:00
try:
status_number = int(rcpt['status'], 16)
except TypeError:
status_number = int(rcpt['status'])
2021-02-24 12:48:11 +01:00
if status_number == 1:
self.status = Status.SUCCESS
2021-02-24 12:48:11 +01:00
elif status_number == 0:
self.status = Status.ERROR
2021-04-04 14:55:27 +02:00
# 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']
2021-04-14 14:45:37 +02:00
try:
self.gas_used = int(rcpt['gasUsed'], 16)
except TypeError:
self.gas_used = int(rcpt['gasUsed'])
2021-02-11 10:16:05 +01:00
2021-02-11 12:43:54 +01:00
def __repr__(self):
2021-02-11 10:16:05 +01:00
return 'block {} tx {} {}'.format(self.block.number, self.index, self.hash)
2021-02-11 12:43:54 +01:00
def __str__(self):
2021-04-04 14:55:27 +02:00
s = """hash {}
2021-02-24 12:48:11 +01:00
from {}
2021-02-12 00:24:10 +01:00
to {}
value {}
nonce {}
gasPrice {}
gasLimit {}
2021-02-24 12:48:11 +01:00
input {}
""".format(
self.hash,
2021-02-12 00:24:10 +01:00
self.outputs[0],
self.inputs[0],
self.value,
self.nonce,
2021-04-08 17:39:32 +02:00
self.gas_price,
self.gas_limit,
2021-02-12 00:24:10 +01:00
self.payload,
)
2021-04-04 14:55:27 +02:00
2021-04-08 17:39:32 +02:00
if self.status != Status.PENDING:
s += """gasUsed {}
""".format(
self.gas_used,
)
2021-04-13 06:50:33 +02:00
s += 'status ' + self.status.name + '\n'
2021-04-08 17:39:32 +02:00
2021-04-04 14:55:27 +02:00
if self.contract != None:
s += """contract {}
""".format(
self.contract,
)
return s