commit f7c1f05a1fc939943abf340d6da67bedb9e6d90a
parent 6ba0ac68209f533969992815c7063cde4c7f3476
Author: nolash <dev@holbrook.no>
Date:   Sat,  9 Jan 2021 22:05:24 +0100
Add transaction executor helper
Diffstat:
8 files changed, 185 insertions(+), 20 deletions(-)
diff --git 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
@@ -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
@@ -0,0 +1 @@
+from .tx import TxExecutor
diff --git a/crypto_dev_signer/helper/tx.py 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
@@ -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
@@ -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
@@ -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
@@ -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, '')