From f7c1f05a1fc939943abf340d6da67bedb9e6d90a Mon Sep 17 00:00:00 2001 From: nolash Date: Sat, 9 Jan 2021 22:05:24 +0100 Subject: [PATCH] Add transaction executor helper --- CHANGELOG | 1 + crypto_dev_signer/error.py | 4 ++ crypto_dev_signer/helper/__init__.py | 1 + crypto_dev_signer/helper/tx.py | 71 +++++++++++++++++++++++++++ crypto_dev_signer/keystore/dict.py | 33 +++++++++++++ setup.py | 2 +- test/test_helper.py | 73 ++++++++++++++++++++++++++++ test/test_keystore_dict.py | 20 +------- 8 files changed, 185 insertions(+), 20 deletions(-) create mode 100644 crypto_dev_signer/helper/__init__.py create mode 100644 crypto_dev_signer/helper/tx.py create mode 100644 crypto_dev_signer/keystore/dict.py create mode 100644 test/test_helper.py diff --git a/CHANGELOG b/CHANGELOG index 7daaddc..25f4354 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ * 0.4.13-unreleased - Implement DictKeystore - Remove unused insert_key method in keystore interface + - Add transaction executor helper * 0.4.12 - Enforce hex strings in signer backend for sign message * 0.4.11 diff --git a/crypto_dev_signer/error.py b/crypto_dev_signer/error.py index 0700a71..ef6a02f 100644 --- a/crypto_dev_signer/error.py +++ b/crypto_dev_signer/error.py @@ -1,2 +1,6 @@ class UnknownAccountError(Exception): pass + + +class TransactionRevertError(Exception): + pass diff --git a/crypto_dev_signer/helper/__init__.py b/crypto_dev_signer/helper/__init__.py new file mode 100644 index 0000000..8ed23de --- /dev/null +++ b/crypto_dev_signer/helper/__init__.py @@ -0,0 +1 @@ +from .tx import TxExecutor diff --git a/crypto_dev_signer/helper/tx.py b/crypto_dev_signer/helper/tx.py new file mode 100644 index 0000000..5456c76 --- /dev/null +++ b/crypto_dev_signer/helper/tx.py @@ -0,0 +1,71 @@ +# standard imports +import logging + +# third-party imports +from crypto_dev_signer.eth.transaction import EIP155Transaction + +# local imports +from crypto_dev_signer.error import TransactionRevertError + +logg = logging.getLogger() + + +class TxExecutor: + + def __init__(self, sender, signer, dispatcher, reporter, nonce, chain_id, fee_helper, fee_price_helper, block=False): + self.sender = sender + self.nonce = nonce + self.signer = signer + self.dispatcher = dispatcher + self.reporter = reporter + self.block = bool(block) + self.chain_id = chain_id + self.tx_hashes = [] + self.fee_price_helper = fee_price_helper + self.fee_helper = fee_helper + + + def sign_and_send(self, builder, force_wait=False): + fee_units = self.fee_helper(self.sender, None, None) + + tx_tpl = { + 'from': self.sender, + 'chainId': self.chain_id, + 'fee': fee_units, + 'feePrice': self.fee_price_helper(), + 'nonce': self.nonce, + } + tx = None + for b in builder: + tx = b(tx_tpl, tx) + + logg.debug('from {} nonce {} tx {}'.format(self.sender, self.nonce, tx)) + + chain_tx = EIP155Transaction(tx, self.nonce, self.chain_id) + signature = self.signer.signTransaction(chain_tx) + chain_tx_serialized = chain_tx.rlp_serialize() + tx_hash = self.dispatcher('0x' + chain_tx_serialized.hex()) + self.tx_hashes.append(tx_hash) + self.nonce += 1 + rcpt = None + if self.block or force_wait: + rcpt = self.wait_for(tx_hash) + logg.info('tx {} fee used: {}'.format(tx_hash.hex(), rcpt['feeUsed'])) + return (tx_hash.hex(), rcpt) + + + def wait_for(self, tx_hash=None): + if tx_hash == None: + tx_hash = self.tx_hashes[len(self.tx_hashes)-1] + i = 1 + while True: + try: + #return self.w3.eth.getTransactionReceipt(tx_hash) + return self.reporter(tx_hash) + except web3.exceptions.TransactionNotFound: + logg.debug('poll #{} for {}'.format(i, tx_hash.hex())) + i += 1 + time.sleep(1) + if rcpt['status'] == 0: + raise TransactionRevertError(tx_hash) + return rcpt diff --git a/crypto_dev_signer/keystore/dict.py b/crypto_dev_signer/keystore/dict.py new file mode 100644 index 0000000..59be2ad --- /dev/null +++ b/crypto_dev_signer/keystore/dict.py @@ -0,0 +1,33 @@ +# standard imports +import logging + +# local imports +from . import keyapi +from .interface import Keystore +from crypto_dev_signer.error import UnknownAccountError +from crypto_dev_signer.common import strip_hex_prefix + +logg = logging.getLogger() + + +class DictKeystore(Keystore): + + def __init__(self): + self.keys = {} + + + def get(self, address, password=None): + if password != None: + logg.debug('password ignored as dictkeystore doesnt do encryption') + try: + return self.keys[address] + except KeyError: + raise UnknownAccountError(address) + + + def import_key(self, pk, password=None): + pubk = keyapi.private_key_to_public_key(pk) + address_hex = pubk.to_checksum_address() + address_hex_clean = strip_hex_prefix(address_hex) + self.keys[address_hex_clean] = pk.to_bytes() + return address_hex diff --git a/setup.py b/setup.py index a6f7a79..9a72cd2 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ f.close() setup( name="crypto-dev-signer", - version="0.4.13a3", + version="0.4.13a4", description="A signer and keystore daemon and library for cryptocurrency software development", author="Louis Holbrook", author_email="dev@holbrook.no", diff --git a/test/test_helper.py b/test/test_helper.py new file mode 100644 index 0000000..82bc611 --- /dev/null +++ b/test/test_helper.py @@ -0,0 +1,73 @@ +# standard imports +import unittest +import logging +import os + +# local imports +from crypto_dev_signer.keystore import DictKeystore +from crypto_dev_signer.eth.signer import ReferenceSigner +from crypto_dev_signer.helper import TxExecutor + +logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + +script_dir = os.path.realpath(os.path.dirname(__file__)) + + +class MockEthTxBackend: + + def dispatcher(self, tx): + logg.debug('sender {}'.format(tx)) + return os.urandom(32) + + def reporter(self, tx): + logg.debug('reporter {}'.format(tx)) + + def fee_price_helper(self): + return 21 + + def fee_helper(self, sender, code, inputs): + logg.debug('fee helper code {} inputs {}'.format(code, inputs)) + return 2 + + def builder(self, a, b): + b = { + 'from': a['from'], + 'to': '0x' + os.urandom(20).hex(), + 'data': '', + 'gasPrice': a['feePrice'], + 'gas': a['fee'], + } + return b + + def builder_two(self, a, b): + b['value'] = 1024 + return b + + +class TestHelper(unittest.TestCase): + + def setUp(self): + logg.debug('setup') + self.db = DictKeystore() + + keystore_filepath = os.path.join(script_dir, 'testdata', 'UTC--2021-01-08T18-37-01.187235289Z--00a329c0648769a73afac7f9381e08fb43dbea72') + + self.address_hex = self.db.import_keystore_file(keystore_filepath, '') + self.signer = ReferenceSigner(self.db) + + + def tearDown(self): + pass + + + def test_helper(self): + backend = MockEthTxBackend() + executor = TxExecutor(self.address_hex, self.signer, backend.dispatcher, backend.reporter, 666, 13, backend.fee_helper, backend.fee_price_helper) + + tx_ish = {'from': self.address_hex} + executor.sign_and_send([backend.builder, backend.builder_two]) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_keystore_dict.py b/test/test_keystore_dict.py index 8094d09..0a7d38c 100644 --- a/test/test_keystore_dict.py +++ b/test/test_keystore_dict.py @@ -6,11 +6,6 @@ import logging import base64 import os -# third-party imports -import psycopg2 -from psycopg2 import sql -from cryptography.fernet import Fernet, InvalidToken - # local imports from crypto_dev_signer.keystore import DictKeystore from crypto_dev_signer.error import UnknownAccountError @@ -22,29 +17,16 @@ logg = logging.getLogger() script_dir = os.path.realpath(os.path.dirname(__file__)) -class TestDatabase(unittest.TestCase): +class TestDict(unittest.TestCase): - conn = None - cur = None - symkey = None address_hex = None db = None def setUp(self): logg.debug('setup') - # arbitrary value - #symkey_hex = 'E92431CAEE69313A7BE9E443C4ABEED9BF8157E9A13553B4D5D6E7D51B5021D9' - #self.symkey = bytes.fromhex(symkey_hex) - - #kw = { - # 'symmetric_key': self.symkey, - # } self.db = DictKeystore() keystore_filepath = os.path.join(script_dir, 'testdata', 'UTC--2021-01-08T18-37-01.187235289Z--00a329c0648769a73afac7f9381e08fb43dbea72') - #f = open( - #s = f.read() - #f.close() self.address_hex = self.db.import_keystore_file(keystore_filepath, '')