diff --git a/python/CHANGELOG b/python/CHANGELOG index e273351..76f80ff 100644 --- a/python/CHANGELOG +++ b/python/CHANGELOG @@ -3,6 +3,7 @@ - Change makefile to compile contract for Byzantium. - Change mint method name to mintTo - Add add minter executable script + - Add decode script * 0.0.6 - Rename contracts-dir flag to abi-dir - Use package data dir as default abi dir diff --git a/python/eth_erc20/erc20.py b/python/eth_erc20/erc20.py index 84ff8f9..98d1c6e 100644 --- a/python/eth_erc20/erc20.py +++ b/python/eth_erc20/erc20.py @@ -32,7 +32,9 @@ logg = logging.getLogger() class ERC20(TxFactory): - + + def gas(self, code=None): + return 8000000 def balance_of(self, contract_address, address, sender_address=ZERO_ADDRESS): o = jsonrpc_template() @@ -164,8 +166,18 @@ class ERC20(TxFactory): return abi_decode_single(ABIContractType.UINT256, v) + @classmethod + def parse_total_supply(self, v): + return abi_decode_single(ABIContractType.UINT256, v) + + @classmethod def parse_balance(self, v): + return self.parse_balance_of(v) + + + @classmethod + def parse_balance_of(self, v): return abi_decode_single(ABIContractType.UINT256, v) diff --git a/python/eth_erc20/runnable/decode.py b/python/eth_erc20/runnable/decode.py new file mode 100644 index 0000000..5391c4f --- /dev/null +++ b/python/eth_erc20/runnable/decode.py @@ -0,0 +1,77 @@ +#!python3 + +"""Decode raw ERC20 transaction + +.. moduleauthor:: Louis Holbrook +.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 + +""" + +# SPDX-License-Identifier: GPL-3.0-or-later + +# standard imports +import sys +import os +import json +import argparse +import logging +import select + +# external imports +from chainlib.eth.tx import unpack +from chainlib.chain import ChainSpec +from chainlib.eth.connection import EthHTTPConnection + +# local imports +from eth_erc20.runnable.util import Decoder + +logging.basicConfig(level=logging.WARNING) +logg = logging.getLogger() + +def stdin_arg(t=0): + h = select.select([sys.stdin], [], [], t) + if len(h[0]) > 0: + logg.debug('got stdin arg') + v = h[0][0].read() + return v.rstrip() + return None + +default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') + +argparser = argparse.ArgumentParser() +argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') +argparser.add_argument('-n', '--no-resolve', dest='n', action='store_true', help='Do not resolve token metadata on-chain') +argparser.add_argument('-i', '--chain-id', dest='i', default='evm:ethereum:1', type=str, help='Numeric network id') +argparser.add_argument('-v', action='store_true', help='Be verbose') +argparser.add_argument('-vv', action='store_true', help='Be more verbose') +argparser.add_argument('tx', type=str, nargs='?', default=stdin_arg(), help='hex-encoded signed raw transaction') +args = argparser.parse_args() + +if args.vv: + logg.setLevel(logging.DEBUG) +elif args.v: + logg.setLevel(logging.INFO) + +argp = args.tx +if argp == None: + logg.info('no argument provided or delayed stdin, attempting another read') + argp = stdin_arg(t=None) + if argp == None: + argparser.error('need first positional argument or value from stdin') + +conn = EthHTTPConnection(args.p) + +chain_spec = ChainSpec.from_chain_str(args.i) + +no_resolve = args.n + +def main(): + tx_raw = argp + resolve_rpc = None + if not no_resolve: + resolve_rpc = conn + dec = Decoder(chain_spec) + dec.decode_for_puny_humans(tx_raw, chain_spec, sys.stdout, 'transfer', rpc=resolve_rpc) + +if __name__ == '__main__': + main() diff --git a/python/eth_erc20/runnable/transfer.py b/python/eth_erc20/runnable/transfer.py index b393937..c2837c8 100644 --- a/python/eth_erc20/runnable/transfer.py +++ b/python/eth_erc20/runnable/transfer.py @@ -10,6 +10,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # standard imports +import sys import os import io import json @@ -29,6 +30,9 @@ from chainlib.eth.nonce import ( RPCNonceOracle, OverrideNonceOracle, ) +from chainlib.eth.tx import ( + unpack, + ) from chainlib.eth.gas import ( RPCGasOracle, OverrideGasOracle, @@ -42,18 +46,17 @@ from eth_erc20 import ERC20 logging.basicConfig(level=logging.WARNING) logg = logging.getLogger() -logging.getLogger('web3').setLevel(logging.WARNING) -logging.getLogger('urllib3').setLevel(logging.WARNING) +default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') -default_abi_dir = '/usr/local/share/cic/solidity/abi' argparser = argparse.ArgumentParser() -argparser.add_argument('-p', '--provider', dest='p', default='http://localhost:8545', type=str, help='Web3 provider url (http only)') +argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') argparser.add_argument('-w', action='store_true', help='Wait for the last transaction to be confirmed') argparser.add_argument('-ww', action='store_true', help='Wait for every transaction to be confirmed') argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') -argparser.add_argument('-a', '--token-address', required='True', dest='a', type=str, help='Token address') -argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Ethereum keystore file to use for signing') +argparser.add_argument('-a', '--token-address', required=True, dest='a', type=str, help='Token address') +argparser.add_argument('-y', '--key-file', dest='y', required=True, type=str, help='Ethereum keystore file to use for signing') argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration') +argparser.add_argument('--data-only', dest='data_only', action='store_true', help='output data part only (does not require key)') argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress') argparser.add_argument('-s', '--send', dest='s', action='store_true', help='Send to network') argparser.add_argument('--nonce', type=int, help='Override nonce') @@ -74,6 +77,10 @@ elif args.v: block_all = args.ww block_last = args.w or block_all +signer = None +signer_address = None +nonce_oracle = None + passphrase_env = 'ETH_PASSPHRASE' if args.env_prefix != None: passphrase_env = args.env_prefix + '_' + passphrase_env @@ -83,7 +90,6 @@ if passphrase == None: logg.warning('no passphrase given') passphrase='' -signer_address = None keystore = DictKeystore() if args.y != None: logg.debug('loading keystore file {}'.format(args.y)) @@ -99,14 +105,12 @@ if args.nonce != None: else: nonce_oracle = RPCNonceOracle(signer_address, conn) -def _max_gas(code=None): - return 8000000 gas_oracle = None if args.gas_price != None or args.gas_limit != None: gas_oracle = OverrideGasOracle(price=args.gas_price, limit=args.gas_limit) else: - gas_oracle = RPCGasOracle(conn, code_callback=_max_gas) + gas_oracle = RPCGasOracle(conn, code_callback=ERC20.gas()) chain_spec = ChainSpec.from_chain_str(args.i) chain_id = chain_spec.network_id() @@ -115,6 +119,8 @@ value = args.amount send = args.s +data_only = args.data_only + g = ERC20(chain_spec, signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle) @@ -135,6 +141,11 @@ def main(): logg.debug('recipient {} balance after: {}'.format(recipient, balance(args.a, recipient))) (tx_hash_hex, o) = g.transfer(args.a, signer_address, recipient, value) + if data_only: + tx_raw_bytes = bytes.fromhex(strip_0x(o['params'][0])) + tx = unpack(tx_raw_bytes, chain_spec) + print(tx['data']) + sys.exit(0) if send: conn.do(o) diff --git a/python/eth_erc20/runnable/util.py b/python/eth_erc20/runnable/util.py new file mode 100644 index 0000000..b933f7f --- /dev/null +++ b/python/eth_erc20/runnable/util.py @@ -0,0 +1,71 @@ +# external imports +from chainlib.eth.tx import ( + unpack, + Tx, + ) +from hexathon import ( + strip_0x, + add_0x, + ) +from chainlib.eth.constant import ZERO_ADDRESS +from chainlib.eth.runnable.util import decode_out + +# local imports +from eth_erc20.erc20 import ERC20 + +sender_address = ZERO_ADDRESS + + +class Decoder: + + def __init__(self, chain_spec): + self.chain_spec = chain_spec + + + def decode_transfer(self, tx, writer, rpc=None): + r = ERC20.parse_transfer_request(tx['data']) + token_value = r[1] + token_str = tx['to'] + token_recipient = r[0] + if rpc != None: + c = ERC20(self.chain_spec) + o = c.symbol(tx['to'], sender_address=sender_address) + r = rpc.do(o) + symbol = c.parse_symbol(r) + + o = c.name(tx['to'], sender_address=sender_address) + r = rpc.do(o) + name = c.parse_symbol(r) + + o = c.decimals(tx['to'], sender_address=sender_address) + r = rpc.do(o) + decimals = c.parse_decimals(r) + + token_str = '{} ({})'.format(name, symbol) + token_value = token_value / (10 ** decimals) + token_value_str = '{:.6f} ({} decimals)'.format(token_value, decimals) + + writer.write("""to: {} +token: {} +value: {} +""".format( + token_recipient, + token_str, + token_value_str, + ) +) + + #writer.write('{}\n'.format(r))k + + + def decode_for_puny_humans(self, tx_raw, chain_spec, writer, method, rpc=None): + tx_raw = strip_0x(tx_raw) + tx_raw_bytes = bytes.fromhex(tx_raw) + tx_src = unpack(tx_raw_bytes, chain_spec) + tx = Tx.src_normalize(tx_src) + + self.decode_transfer(tx, writer, rpc=rpc) + + decode_out(tx_src, writer, skip_keys=['to']) + + writer.write('src: {}\n'.format(add_0x(tx_raw))) diff --git a/python/setup.cfg b/python/setup.cfg index 2f2a956..46fe415 100644 --- a/python/setup.cfg +++ b/python/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = eth-erc20 -version = 0.0.9a2 +version = 0.0.9a5 description = ERC20 interface and simple contract with deployment script that lets any address mint and gift itself tokens. author = Louis Holbrook author_email = dev@holbrook.no @@ -44,3 +44,4 @@ console_scripts = giftable-token-minter = giftable_erc20_token.runnable.minter:main erc20-transfer = eth_erc20.runnable.transfer:main erc20-balance = eth_erc20.runnable.balance:main + erc20-decode = eth_erc20.runnable.decode:main