chainlib-eth/chainlib/eth/gas.py

256 lines
8.8 KiB
Python

# standard imports
import logging
# external imports
from hexathon import (
add_0x,
strip_0x,
)
from funga.eth.transaction import EIP155Transaction
# local imports
from chainlib.fee import FeeOracle
from chainlib.hash import keccak256_hex_to_hex
from chainlib.jsonrpc import JSONRPCRequest
from chainlib.eth.tx import (
TxFactory,
TxFormat,
raw,
)
from chainlib.eth.jsonrpc import to_blockheight_param
from chainlib.block import BlockSpec
from chainlib.eth.constant import (
MINIMUM_FEE_UNITS,
)
logg = logging.getLogger(__name__)
def price(id_generator=None):
"""Generate json-rpc query to retrieve current network gas price guess from node.
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_gasPrice'
return j.finalize(o)
def balance(address, id_generator=None, height=BlockSpec.LATEST):
"""Generate json-rpc query to retrieve gas balance of address.
:param address: Address to query balance for, in hex
:type address: str
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
:param height: Block height specifier
:type height: chainlib.block.BlockSpec
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_getBalance'
o['params'].append(add_0x(address))
height = to_blockheight_param(height)
o['params'].append(height)
return j.finalize(o)
def parse_balance(balance):
"""Parse result of chainlib.eth.gas.balance rpc query
:param balance: rpc result value, in hex or int
:type balance: any
:rtype: int
:returns: Balance integer value
"""
try:
r = int(balance, 10)
except ValueError:
r = int(balance, 16)
return r
class Gas(TxFactory):
"""Gas transaction helper.
"""
def create(self, sender_address, recipient_address, value, data=None, tx_format=TxFormat.JSONRPC, id_generator=None):
"""Generate json-rpc query to execute gas transaction.
See parent class TxFactory for details on output format and general usage.
:param sender_address: Sender address, in hex
:type sender_address: str
:param recipient_address: Recipient address, in hex
:type recipient_address: str
:param value: Value of transaction, integer decimal value (wei)
:type value: int
:param data: Arbitrary input data, in hex. None means no data (vanilla gas transaction).
:type data: str
:param tx_format: Output format
:type tx_format: chainlib.eth.tx.TxFormat
"""
tx = self.template(sender_address, recipient_address, use_nonce=True)
tx['value'] = value
if data != None:
tx['data'] = data
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))
o = None
if tx_format == TxFormat.JSONRPC:
o = raw(tx_raw_hex, id_generator=id_generator)
elif tx_format == TxFormat.RLP_SIGNED:
o = tx_raw_hex
return (tx_hash_hex, o)
class RPCGasOracle(FeeOracle):
"""JSON-RPC only gas parameter helper.
:param conn: RPC connection
:type conn: chainlib.connection.RPCConnection
:param code_callback: Callback method to evaluate gas usage for method and inputs.
:type code_callback: method taking abi encoded input data as single argument
:param min_price: Override gas price if less than given value
:type min_price: int
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
"""
def __init__(self, conn, code_callback=None, min_price=1, id_generator=None):
super(RPCGasOracle, self).__init__(code_callback=code_callback)
self.conn = conn
self.min_price = min_price
self.id_generator = id_generator
def get_fee(self, code=None, input_data=None):
"""Retrieve gas parameters from node.
If code is given, the set code callback will be used to estimate gas usage.
If code is not given or code callback is not set, the chainlib.eth.constant.MINIMUM_FEE_UNITS constant will be used. This gas limit will only be enough gas for a gas transaction without input data.
:param code: EVM execution code to evaluate against, in hex
:type code: str
:param input_data: Contract input data, in hex
:type input_data: str
:rtype: tuple
:returns: Gas price in wei, and gas limit in gas units
"""
gas_price = 0
if self.conn != None:
o = price(id_generator=self.id_generator)
r = self.conn.do(o)
n = strip_0x(r)
gas_price = int(n, 16)
fee_units = MINIMUM_FEE_UNITS
if self.code_callback != None:
fee_units = self.code_callback(code)
if gas_price < self.min_price:
logg.debug('adjusting price {} to set minimum {}'.format(gas_price, self.min_price))
gas_price = self.min_price
return (gas_price, fee_units)
def get_gas(self, code=None, input_data=None):
return self.get_fee(code=code, input_data=input_data)
class RPCPureGasOracle(RPCGasOracle):
"""Convenience constructor for rpc gas oracle without minimum price.
:param conn: RPC connection
:type conn: chainlib.connection.RPCConnection
:param code_callback: Callback method to evaluate gas usage for method and inputs.
:type code_callback: method taking abi encoded input data as single argument
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
"""
def __init__(self, conn, code_callback=None, id_generator=None):
super(RPCPureGasOracle, self).__init__(conn, code_callback=code_callback, min_price=0, id_generator=id_generator)
class OverrideGasOracle(RPCGasOracle):
"""Gas parameter helper that can be selectively overridden.
If both price and limit are set, the conn parameter will not be used.
If either price or limit is set to None, the rpc in the conn value will be used to query the missing value.
If both are None, behaves the same as chainlib.eth.gas.RPCGasOracle.
:param price: Set exact gas price
:type price: int
:param limit: Set exact gas limit
:type limit: int
:param conn: RPC connection for fallback query
:type conn: chainlib.connection.RPCConnection
:param code_callback: Callback method to evaluate gas usage for method and inputs.
:type code_callback: method taking abi encoded input data as single argument
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
"""
def __init__(self, price=None, limit=None, conn=None, code_callback=None, id_generator=None):
self.conn = None
self.code_callback = None
self.limit = limit
self.price = price
price_conn = None
if self.limit == None or self.price == None:
if self.price == None:
price_conn = conn
logg.debug('override gas oracle with rpc fallback; price {} limit {}'.format(self.price, self.limit))
super(OverrideGasOracle, self).__init__(price_conn, code_callback, id_generator=id_generator)
def get_fee(self, code=None, input_data=None):
r = None
fee_units = None
fee_price = None
rpc_results = super(OverrideGasOracle, self).get_fee(code)
if self.limit != None:
fee_units = self.limit
if self.price != None:
fee_price = self.price
if fee_price == None:
if rpc_results != None:
fee_price = rpc_results[0]
logg.debug('override gas oracle without explicit price, setting from rpc {}'.format(fee_price))
else:
fee_price = MINIMUM_FEE_PRICE
logg.debug('override gas oracle without explicit price, setting default {}'.format(fee_price))
if fee_units == None:
if rpc_results != None:
fee_units = rpc_results[1]
logg.debug('override gas oracle without explicit limit, setting from rpc {}'.format(fee_units))
else:
fee_units = MINIMUM_FEE_UNITS
logg.debug('override gas oracle without explicit limit, setting default {}'.format(fee_units))
return (fee_price, fee_units)
def get_gas(self, code=None, input_data=None):
return self.get_fee(code=code, input_data=input_data)
DefaultGasOracle = RPCGasOracle