From 8be9b595565bd8d1638390221391fbc710168da4 Mon Sep 17 00:00:00 2001 From: nolash Date: Tue, 9 Feb 2021 12:12:37 +0100 Subject: [PATCH] WIP add erc20 transfer executable --- MANIFEST.in | 1 + cic_tools/eth/connection.py | 2 +- cic_tools/eth/constant.py | 1 + cic_tools/eth/erc20.py | 69 ++++++++++++++++++ cic_tools/eth/gas.py | 4 +- cic_tools/eth/method.py | 61 ---------------- cic_tools/eth/nonce.py | 4 +- cic_tools/eth/rpc.py | 17 +++++ cic_tools/eth/runnable/balance.py | 17 +++-- cic_tools/eth/runnable/gas.py | 13 ++-- cic_tools/eth/runnable/transfer.py | 113 +++++++++++++++++++++++++++++ cic_tools/eth/tx.py | 37 +++++++++- requirements.txt | 14 ++-- 13 files changed, 261 insertions(+), 92 deletions(-) create mode 100644 MANIFEST.in create mode 100644 cic_tools/eth/erc20.py delete mode 100644 cic_tools/eth/method.py create mode 100644 cic_tools/eth/rpc.py create mode 100644 cic_tools/eth/runnable/transfer.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..f9bd145 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include requirements.txt diff --git a/cic_tools/eth/connection.py b/cic_tools/eth/connection.py index 810639b..c9c60d9 100644 --- a/cic_tools/eth/connection.py +++ b/cic_tools/eth/connection.py @@ -6,7 +6,7 @@ from urllib.request import ( ) from .error import DefaultErrorParser -from .method import jsonrpc_result +from .rpc import jsonrpc_result error_parser = DefaultErrorParser() logg = logging.getLogger(__name__) diff --git a/cic_tools/eth/constant.py b/cic_tools/eth/constant.py index 675df41..6dd2910 100644 --- a/cic_tools/eth/constant.py +++ b/cic_tools/eth/constant.py @@ -1,3 +1,4 @@ ZERO_ADDRESS = '0x{:040x}'.format(0) ZERO_CONTENT = '0x{:064x}'.format(0) MINIMUM_FEE_UNITS = 21000 +MINIMUM_FEE_PRICE = 1000000000 diff --git a/cic_tools/eth/erc20.py b/cic_tools/eth/erc20.py new file mode 100644 index 0000000..dba156b --- /dev/null +++ b/cic_tools/eth/erc20.py @@ -0,0 +1,69 @@ +# third-party imports +import sha3 +from hexathon import add_0x +from eth_abi import encode_single + +# local imports +from .hash import keccak256_string_to_hex +from .constant import ZERO_ADDRESS +from .rpc import jsonrpc_template +from .tx import TxFactory + + +# TODO: move to cic-contracts +erc20_balance_signature = keccak256_string_to_hex('balanceOf(address)')[:8] +erc20_decimals_signature = keccak256_string_to_hex('decimals()')[:8] +erc20_transfer_signature = keccak256_string_to_hex('transfer(address,uint256')[:8] + + +class ERC20TxFactory(TxFactory): + + def build(self, tx): + txe = EIP155Transaction(tx, tx['nonce'], tx['chainId']) + self.signer.signTransaction(txe) + tx_raw = txe.rlp_serialize() + tx_raw_hex = add_0x(tx_raw.hex()) + tx_hash_hex = add_0x(keccak256_hex_to_hex(tx_raw_hex)) + + o = jsonrpc_template() + o['method'] = 'eth_sendRawTransaction' + o['params'].append(tx_raw_hex) + + return (tx_hash_hex, o) + + + def erc20_balance(self, contract_address, address, sender_address=ZERO_ADDRESS): + o = jsonrpc_template() + o['method'] = 'eth_call' + data = erc20_balance_signature + data += encode_single('address', address).hex() + data = add_0x(data) + 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 + + + def erc20_decimals(self, contract_address, sender_address=ZERO_ADDRESS): + o = jsonrpc_template() + o['method'] = 'eth_call' + data = add_0x(erc20_decimals_signature) + tx = self.template(sender_address, contract_address) + tx = self.set_code(tx, data, update_fee=False) + o['params'].append(self.normalize(tx)) + o['params'].append('latest') + return o + + + def erc20_transfer(self, contract_address, sender_address, value): + o = jsonrpc_template() + o['method'] = 'eth_sendRawTransaction' + data = erc20_transfer_signature + data += encode_single('address', sender_address).hex() + data += encode_single('uint256', value).hex() + data = add_0x(data) + tx = self.template(sender_address, contract_address) + tx = self.set_code(tx, data) + o['params'].append(tx) + return self.build(o) diff --git a/cic_tools/eth/gas.py b/cic_tools/eth/gas.py index 44bf487..3472d62 100644 --- a/cic_tools/eth/gas.py +++ b/cic_tools/eth/gas.py @@ -6,9 +6,7 @@ from hexathon import ( from crypto_dev_signer.eth.transaction import EIP155Transaction # local imports -from cic_tools.eth.method import ( - jsonrpc_template, - ) +from cic_tools.eth.rpc import jsonrpc_template from cic_tools.eth.tx import TxFactory from cic_tools.eth.hash import keccak256_hex_to_hex diff --git a/cic_tools/eth/method.py b/cic_tools/eth/method.py deleted file mode 100644 index b80a0ff..0000000 --- a/cic_tools/eth/method.py +++ /dev/null @@ -1,61 +0,0 @@ -import sha3 -import uuid - -from hexathon import add_0x -from eth_abi import encode_single - -from .hash import keccak256_string_to_hex -from .constant import ZERO_ADDRESS - - -# TODO: move to cic-contracts -erc20_balance_signature = keccak256_string_to_hex('balanceOf(address)')[:8] -erc20_decimals_signature = keccak256_string_to_hex('decimals()')[:8] - - -def jsonrpc_template(): - return { - 'jsonrpc': '2.0', - 'id': str(uuid.uuid4()), - 'method': None, - 'params': [], - } - - -def erc20_balance(contract_address, address, sender_address=ZERO_ADDRESS): - o = jsonrpc_template() - o['method'] = 'eth_call' - data = erc20_balance_signature - data += encode_single('address', address).hex() - data = add_0x(data) - a = call(contract_address, data=data) - o['params'].append(a) - o['params'].append('latest') - return o - - -def erc20_decimals(contract_address, sender_address=ZERO_ADDRESS): - o = jsonrpc_template() - o['method'] = 'eth_call' - arg = add_0x(erc20_decimals_signature) - #o['params'].append(arg) - a = call(contract_address, arg) - o['params'].append(a) - o['params'].append('latest') - return o - - -def call(contract_address, data, sender_address=ZERO_ADDRESS): - return { - 'from': sender_address, - 'to': contract_address, - 'data': data, - } - - -def jsonrpc_result(o, ep): - if o.get('error') != None: - raise ep.translate(o) - return o['result'] - - diff --git a/cic_tools/eth/nonce.py b/cic_tools/eth/nonce.py index 1a1a10f..0831564 100644 --- a/cic_tools/eth/nonce.py +++ b/cic_tools/eth/nonce.py @@ -5,9 +5,7 @@ from hexathon import ( ) # local imports -from cic_tools.eth.method import ( - jsonrpc_template, - ) +from cic_tools.eth.rpc import jsonrpc_template def nonce(address): diff --git a/cic_tools/eth/rpc.py b/cic_tools/eth/rpc.py new file mode 100644 index 0000000..8698494 --- /dev/null +++ b/cic_tools/eth/rpc.py @@ -0,0 +1,17 @@ +# standard imports +import uuid + + +def jsonrpc_template(): + return { + 'jsonrpc': '2.0', + 'id': str(uuid.uuid4()), + 'method': None, + 'params': [], + } + + +def jsonrpc_result(o, ep): + if o.get('error') != None: + raise ep.translate(o) + return o['result'] diff --git a/cic_tools/eth/runnable/balance.py b/cic_tools/eth/runnable/balance.py index bd3c569..4259585 100644 --- a/cic_tools/eth/runnable/balance.py +++ b/cic_tools/eth/runnable/balance.py @@ -26,14 +26,14 @@ from eth_abi import encode_single # local imports from cic_tools.eth.address import to_checksum -from cic_tools.eth.method import ( +from cic_tools.eth.rpc import ( jsonrpc_template, - erc20_balance, - erc20_decimals, jsonrpc_result, ) +from cic_tools.eth.erc20 import ERC20TxFactory from cic_tools.eth.connection import HTTPConnection - +from cic_tools.eth.nonce import DefaultNonceOracle +from cic_tools.eth.gas import DefaultGasOracle logging.basicConfig(level=logging.WARNING) logg = logging.getLogger() @@ -55,22 +55,25 @@ if args.v: logg.setLevel(logging.DEBUG) conn = HTTPConnection(args.p) +gas_oracle = DefaultGasOracle(conn) + def main(): account = to_checksum(args.account) if not args.u and account != add_0x(args.account): raise ValueError('invalid checksum address') - + r = None decimals = 18 if args.t != None: + g = ERC20TxFactory() # determine decimals - decimals_o = erc20_decimals(args.t) + decimals_o = g.erc20_decimals(args.t) r = conn.do(decimals_o) decimals = int(strip_0x(r), 16) # get balance - balance_o = erc20_balance(args.t, account) + balance_o = g.erc20_balance(args.t, account) r = conn.do(balance_o) else: diff --git a/cic_tools/eth/runnable/gas.py b/cic_tools/eth/runnable/gas.py index 59171dd..47bfb29 100644 --- a/cic_tools/eth/runnable/gas.py +++ b/cic_tools/eth/runnable/gas.py @@ -19,7 +19,6 @@ import logging # third-party imports from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer from crypto_dev_signer.keystore import DictKeystore -from crypto_dev_signer.eth.helper import EthTxExecutor from hexathon import ( add_0x, strip_0x, @@ -28,9 +27,7 @@ from hexathon import ( # local imports from cic_tools.eth.address import to_checksum from cic_tools.eth.connection import HTTPConnection -from cic_tools.eth.method import ( - jsonrpc_template, - ) +from cic_tools.eth.rpc import jsonrpc_template from cic_tools.eth.nonce import DefaultNonceOracle from cic_tools.eth.gas import ( DefaultGasOracle, @@ -52,8 +49,8 @@ argparser.add_argument('-ww', action='store_true', help='Wait for every transact argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='Ethereum:1', help='Chain specification string') argparser.add_argument('-a', '--signer-address', dest='a', type=str, help='Signing address') argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Ethereum keystore file to use for signing') -argparser.add_argument('-v', action='store_true', help='Be verbose') argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress') +argparser.add_argument('-v', action='store_true', help='Be verbose') argparser.add_argument('-vv', action='store_true', help='Be more verbose') argparser.add_argument('recipient', type=str, help='Ethereum address of recipient') argparser.add_argument('amount', type=int, help='Amount of tokens to mint and gift') @@ -101,10 +98,14 @@ def main(): logg.debug('sender {} balance before: {}'.format(signer_address, balance(signer_address))) logg.debug('recipient {} balance before: {}'.format(recipient, balance(recipient))) - g = GasTxFactory(signer, gas_oracle, nonce_oracle, chain_id=chain_id) + g = GasTxFactory(signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle, chain_id=chain_id) (tx_hash_hex, o) = g.create(signer_address, recipient, value) conn.do(o) + logg.debug('sender {} balance after: {}'.format(signer_address, balance(signer_address))) + logg.debug('recipient {} balance after: {}'.format(recipient, balance(recipient))) + + print(tx_hash_hex) diff --git a/cic_tools/eth/runnable/transfer.py b/cic_tools/eth/runnable/transfer.py new file mode 100644 index 0000000..09de979 --- /dev/null +++ b/cic_tools/eth/runnable/transfer.py @@ -0,0 +1,113 @@ +#!python3 + +"""Token transfer script + +.. moduleauthor:: Louis Holbrook +.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 + +""" + +# SPDX-License-Identifier: GPL-3.0-or-later + +# standard imports +import os +import json +import argparse +import logging + +# third-party imports +from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer +from crypto_dev_signer.keystore import DictKeystore +from crypto_dev_signer.eth.helper import EthTxExecutor +from hexathon import ( + add_0x, + strip_0x, + ) + +# local imports +from cic_tools.eth.address import to_checksum +from cic_tools.eth.connection import HTTPConnection +from cic_tools.eth.rpc import jsonrpc_template +from cic_tools.eth.nonce import DefaultNonceOracle +from cic_tools.eth.gas import DefaultGasOracle +from cic_tools.eth.erc20 import ERC20TxFactory + + +logging.basicConfig(level=logging.WARNING) +logg = logging.getLogger() + +logging.getLogger('web3').setLevel(logging.WARNING) +logging.getLogger('urllib3').setLevel(logging.WARNING) + +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('-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='Ethereum:1', help='Chain specification string') +argparser.add_argument('--token-address', required='True', dest='t', type=str, help='Token address') +argparser.add_argument('-a', '--sender-address', dest='s', type=str, help='Sender account address') +argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Ethereum keystore file to use for signing') +argparser.add_argument('--abi-dir', dest='abi_dir', type=str, default=default_abi_dir, help='Directory containing bytecode and abi (default {})'.format(default_abi_dir)) +argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress') +argparser.add_argument('-v', action='store_true', help='Be verbose') +argparser.add_argument('-vv', action='store_true', help='Be more verbose') +argparser.add_argument('recipient', type=str, help='Recipient account address') +argparser.add_argument('amount', type=int, help='Amount of tokens to mint and gift') +args = argparser.parse_args() + + +if args.vv: + logg.setLevel(logging.DEBUG) +elif args.v: + logg.setLevel(logging.INFO) + +block_last = args.w +block_all = args.ww + +signer_address = None +keystore = DictKeystore() +if args.y != None: + logg.debug('loading keystore file {}'.format(args.y)) + signer_address = keystore.import_keystore_file(args.y) + logg.debug('now have key for signer address {}'.format(signer_address)) +signer = EIP155Signer(keystore) + +conn = HTTPConnection(args.p) +nonce_oracle = DefaultNonceOracle(signer_address, conn) +gas_oracle = DefaultGasOracle(conn) + +chain_pair = args.i.split(':') +chain_id = int(chain_pair[1]) +#g = ERC20TxFactory(signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle, chain_id=chain_id) +g = ERC20TxFactory() + +def balance(token_address, address): + o = g.erc20_balance(token_address, address) + r = conn.do(o) + hx = strip_0x(r) + return int(hx, 16) + + +def main(): + recipient = args.recipient + if not args.u and recipient != add_0x(args.recipient): + raise ValueError('invalid checksum address') + + value = args.amount + + logg.debug('sender {} balance before: {}'.format(signer_address, balance(args.t, signer_address))) + logg.debug('recipient {} balance before: {}'.format(recipient, balance(args.t, recipient))) + + logg.debug('sender {} balance after: {}'.format(signer_address, balance(args.t, signer_address))) + logg.debug('recipient {} balance after: {}'.format(recipient, balance(args.t, recipient))) + + + if block_last: + helper.wait_for() + + print(tx_hash) + + +if __name__ == '__main__': + main() diff --git a/cic_tools/eth/tx.py b/cic_tools/eth/tx.py index 9d98cd8..57a6c8f 100644 --- a/cic_tools/eth/tx.py +++ b/cic_tools/eth/tx.py @@ -7,10 +7,14 @@ from eth_keys import KeyAPI from eth_keys.backends import NativeECCBackend from rlp import decode as rlp_decode from rlp import encode as rlp_encode +from crypto_dev_signer.eth.transaction import EIP155Transaction # local imports from .address import to_checksum -from .constant import MINIMUM_FEE_UNITS +from .constant import ( + MINIMUM_FEE_UNITS, + MINIMUM_FEE_PRICE, + ) logg = logging.getLogger(__name__) @@ -84,7 +88,7 @@ def unpack_signed(tx_raw_bytes, chain_id=1): class TxFactory: - def __init__(self, signer, gas_oracle, nonce_oracle, chain_id=1): + def __init__(self, signer=None, gas_oracle=None, nonce_oracle=None, chain_id=1): self.gas_oracle = gas_oracle self.nonce_oracle = nonce_oracle self.chain_id = chain_id @@ -92,9 +96,13 @@ class TxFactory: def template(self, sender, recipient): - gas_price = self.gas_oracle.get() + gas_price = MINIMUM_FEE_PRICE + if self.gas_oracle != None: + gas_price = self.gas_oracle.get() logg.debug('using gas price {}'.format(gas_price)) - nonce = self.nonce_oracle.next() + nonce = 0 + if self.nonce_oracle != None: + nonce = self.nonce_oracle.next() logg.debug('using nonce {} for address {}'.format(nonce, sender)) return { 'from': sender, @@ -106,3 +114,24 @@ class TxFactory: 'gas': MINIMUM_FEE_UNITS, 'chainId': self.chain_id, } + + + def normalize(self, tx): + txe = EIP155Transaction(tx, tx['nonce'], tx['chainId']) + txes = txe.serialize() + print(txes) + return { + 'from': tx['from'], + 'to': txes['to'], + 'gasPrice': txes['gasPrice'], + 'gas': txes['gas'], + 'data': txes['data'], + } + + + def set_code(self, tx, data, update_fee=True): + tx['data'] = data + if update_fee: + logg.debug('using hardcoded gas limit of 8000000 until we have reliable vm executor') + tx['gas'] = 8000000 + return tx diff --git a/requirements.txt b/requirements.txt index 968118c..bd54b0a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ cryptocurrency-cli-tools==0.0.4 -giftable-erc20-token==0.0.7b6 -eth-accounts-index==0.0.10a6 -erc20-single-shot-faucet==0.2.0a5 -erc20-approval-escrow==0.3.0a4 -cic-eth==0.10.0a21+build.e4161b3e +giftable-erc20-token~=0.0.7b7 +eth-accounts-index~=0.0.10a7 +erc20-single-shot-faucet~=0.2.0a6 +erc20-approval-escrow~=0.3.0a5 +cic-eth~=0.10.0a25 vobject==0.9.6.1 faker==4.17.1 -eth-address-index==0.1.0a5 -crypto-dev-signer==0.4.13rc2 +eth-address-index~=0.1.0a6 +crypto-dev-signer~=0.4.13rc2 pysha3==1.0.2 hexathon==0.0.1a2 eth-abi==2.1.1