diff --git a/bin/libbfa/README.md b/bin/libbfa/README.md index e5ccf3b28317b36d6e641d9deb80f2f7a45d3b9a..3d2ab36d0849c0e61599d6218897a9c3290ede76 100755 --- a/bin/libbfa/README.md +++ b/bin/libbfa/README.md @@ -126,3 +126,10 @@ print(majorcontr.functions.councilLength.call()) # Fix print(majorcontr.functions.councilLength().call()) ``` + +<a name="argument-after-asterisk-must-be-an-iterable-not-nonetype"></a> +gas required exceeds allowance +```python +# Error text: ValueError: {'code': -32000, 'message': 'gas required exceeds allowance (8000000)'} +``` +Tu transacción va a fallar. diff --git a/bin/libbfa/__init__.py b/bin/libbfa/__init__.py index e22c7f41188f07df10965869913ebb2fcf5b1426..c06f92c2d725b6c84a540aeb94232cf88861aa67 100755 --- a/bin/libbfa/__init__.py +++ b/bin/libbfa/__init__.py @@ -8,6 +8,7 @@ import web3.exceptions import web3.middleware import ecdsa from Crypto.Hash import keccak +import eth_account; class Bfa: @@ -19,9 +20,9 @@ class Bfa: elif 'HOME' in os.environ and os.path.isdir(os.path.join(os.environ['HOME'], 'bfa')): os.putenv('BFAHOME', os.path.join(os.environ['HOME'], 'bfa')) if isinstance(provider, str): - if provider in ['prod', 'bfa2018' 'network']: + if provider in ['prod', 'bfa2018', 'network', '']: provider = web3.HTTPProvider("http://public.bfa.ar:8545/") - elif provider in ['test2network', 'test2', 'test', '']: + elif provider in ['test2network', 'test2', 'test']: provider = web3.HTTPProvider("http://public.test2.bfa.ar:8545/") elif provider.startswith('http://') or provider.startswith('https://'): provider = web3.HTTPProvider(provider) @@ -35,20 +36,64 @@ class Bfa: class Account: - def __init__(self, accountname: str, passphrase: str = ''): + def __init__(self, *args): + if len(args) == 0: + self.new() + return + accountname = None + passphrase = '' + if len(args) >= 1: + accountname = args[0] + if len(args) >= 2: + passphrase = args[1] self.keyfile = None - self.privatekey = None + self.key = None self.unlock(accountname, passphrase) self.nonce = 0 def __repr__(self) -> str: - return str(dict(keyfile=self.keyfile, privatekey=str(self.privatekey))) + return str(dict(keyfile=self.keyfile, key=str(self.key))) def __str__(self) -> str: - return self.walletaddress() + return self.address + + def new(self): + acct = eth_account.Account.create() + self.address = acct.address + self.key = acct.key + self.save() + # print(acct.address) + # print(acct.key.hex()) + return acct + + def save(self): + dir = None + if os.getenv('BFANODEDIR') and os.path.exists(os.path.join(os.environ['BFANODEDIR'], 'keystore')): + dir = os.path.join(os.environ['BFANODEDIR'], 'keystore') + elif os.getenv('BFANETWORKDIR') and os.path.exists(os.path.join(os.environ['BFANETWORKDIR'], 'node', 'keystore')): + dir = os.path.join(os.environ['BFANETWORKDIR'], 'node', 'keystore') + elif os.getenv('BFAHOME') and os.path.exists(os.path.join(os.environ['BFAHOME'], 'network', 'node', 'keystore')): + dir = os.path.join(os.environ['BFAHOME'], 'network', 'node', 'keystore') + elif os.getenv('HOME'): + dir = os.path.join(os.environ['HOME'], '.ethereum', 'keystore') + os.makedirs(dir, mode=0o700, exist_ok=True) + else: + raise OSError('I have no idea where to save the file.') + self.keyfile = os.path.join(dir, self.address) + encrypted = eth_account.Account.encrypt(self.key.hex(), '') + try: + with open(self.keyfile, 'w', encoding='utf=8') as outfile: + outfile.write(str(encrypted).replace("'", '"')) + except: + # os.remove(filename) + raise + pass @staticmethod - def findkeyfilesindirectories(pattern: str): + def findkeyfilesindirectories(**kwargs): + pattern = '' + if 'pattern' in kwargs: + pattern = kwargs['pattern'] # Remove leading 0x if present if pattern.startswith('0x'): pattern = pattern[2:] @@ -61,7 +106,7 @@ class Account: elif os.getenv('BFANETWORKDIR'): wheretolook += [os.path.join(os.environ['BFANETWORKDIR'], 'node', 'keystore')] elif os.getenv('BFAHOME'): - wheretolook += [os.path.join(os.environ['BFANETWORKDIR'], 'network', 'node', 'keystore')] + wheretolook += [os.path.join(os.environ['BFAHOME'], 'network', 'node', 'keystore')] if os.getenv('HOME'): wheretolook += [ os.path.join(os.environ['HOME'], '.ethereum', 'keystore'), @@ -95,35 +140,33 @@ class Account: if os.path.isfile(accountname): self.keyfile = accountname else: - self.keyfile = self.findkeyfilesindirectories(accountname) + self.keyfile = self.findkeyfilesindirectories(pattern=accountname) if self.keyfile is None: raise FileNotFoundError('The account was not found.') with open(self.keyfile) as fd: encrypted_key = fd.read() try: - self.privatekey = web3.Web3().eth.account.decrypt(encrypted_key, passphrase) + self.key = web3.Web3().eth.account.decrypt(encrypted_key, passphrase) except ValueError as exc: raise ValueError( - 'The passphrase given for the account is incorrect, ' - 'or the input file is not a valid key file.' + 'The passphrase given for the account in file {} is incorrect, ' + 'or the input file is not a valid key file.'.format(self.keyfile) ) from exc - - def publickey(self) -> bytearray: - key = ecdsa.SigningKey.from_string(self.privatekey, curve=ecdsa.SECP256k1).verifying_key - # returns bytestring - return key.to_string() - - def walletaddress(self) -> str: + # ADDRESS + publickey = ecdsa.SigningKey.from_string(self.key, curve=ecdsa.SECP256k1).verifying_key + pkbytestring = publickey.to_string() # returns bytestring ourhash = keccak.new(digest_bits=256) - ourhash.update(self.publickey()) + ourhash.update(pkbytestring) digest = ourhash.hexdigest() - return web3.Web3().toChecksumAddress('0x' + digest[-40:]) + self.address = web3.Web3().toChecksumAddress('0x' + digest[-40:]) def transact(self, *args, **kwargs): w3 = kwargs.get('web3') + tx_details = self.calculate_tx_details(w3, *args, **kwargs) afunction = kwargs.get('function') - tx_details = self.calculate_tx_details(w3, afunction, *args) - txobj = afunction(*args).buildTransaction(tx_details) + txobj = tx_details + if afunction: + txobj = afunction(*args).buildTransaction(tx_details) receipt = self.txsignsendwait(w3, txobj) return receipt @@ -135,38 +178,53 @@ class Account: return receipt def signtx(self, tx: dict): - signed = web3.Web3().eth.account.sign_transaction(tx, self.privatekey) + signed = web3.Web3().eth.account.sign_transaction(tx, self.key) return signed - def calculate_tx_details(self, w3: web3.Web3, afunction, *args) -> dict: + # if kwargs has extragas=50000 then we add that number to the amount + # of gas for the transaction + def calculate_tx_details(self, w3: web3.Web3, *args, **kwargs) -> dict: # Nonce may have increase on the network without us noticing # or past transactions may not yet have been mined (and a flooded # txpool). # This is a resonable fix (try not to send too many transactions) # If you use waitForTransactionReceipt() between each transaction # you will not have problems because of this. - self.nonce = max(self.nonce, w3.eth.getTransactionCount(self.walletaddress())) + afunction = kwargs.get('function') + self.nonce = max(self.nonce, w3.eth.getTransactionCount(self.address)) # Set minimum gasPrice to 1 Gwei, but allow more if the network says so. details = { 'chainId': w3.eth.chain_id(), 'gasPrice': min(w3.toWei('1', 'gwei'), w3.eth.gasPrice), 'nonce': self.nonce, - 'from': self.walletaddress(), + 'from': self.address, } + for kw in [ 'to', 'value' ]: + val = kwargs.get(kw) + if val is not None: + details[kw] = val # Ask for balance, so we can tell it, in case we have an exception - balance = w3.eth.getBalance(self.walletaddress()) + balance = w3.eth.getBalance(self.address) try: - # Ask a node how much gas it would cost to deploy - gas = afunction(*args).estimateGas(details) + if afunction: + # Ask a node how much gas it would cost to deploy + gas = afunction(*args).estimateGas(details) + else: + gas = w3.eth.estimateGas(details) except web3.exceptions.SolidityError as exc: raise web3.exceptions.SolidityError( 'The Ethereum Virtual Machine probably did not like that.' ) from exc except ValueError as exc: raise ValueError( - 'Your account may not have enough balance to work on this ' - 'network. It currently has {} wei.' + 'Your transaction will fail. Maybe you are calling your ' + 'contract wrong or are not allowed to call the funcion ' + 'by a function modifier or ' + 'your account may not have enough balance to work on this ' + 'network. Your account balance is currently {} wei.' .format(balance)) from exc + if kwargs.get('extragas') is not None: + gas += kwargs.get('extragas') details['gas'] = gas return details @@ -258,9 +316,11 @@ class CompiledContract: with open('{}.sol'.format(self.name), 'r') as infile: with open('contract.sol', 'w') as outfile: outfile.write(infile.read()) - solc = subprocess.run(self.dockerargs(), stdout=subprocess.PIPE, check=True) - # Don't leave too much mess. - os.remove('contract.sol') + try: + solc = subprocess.run(self.dockerargs(), stdout=subprocess.PIPE, check=True) + finally: + # Don't leave too much mess. + os.remove('contract.sol') txt = solc.stdout output = txt.decode('utf-8') self.json = json.loads(output) @@ -273,7 +333,7 @@ class CompiledContract: except FileNotFoundError: return if len(output) < 2: - print("The JSON file is too little ({} bytes read from {}).".format(len(output), filename), file=sys.stderr) + print("The JSON file is too small ({} bytes read from {}).".format(len(output), filename), file=sys.stderr) raise NameError try: self.json = json.loads(output)