def test_add_signature_to_transaction_with_netowrk_id(self): for network_id in [1, 2, 66, 100]: sender_private_key = "0x0164f7c7399f4bb1eafeaae699ebbb12050bc6a50b2836b9ca766068a9d000c0" sender_address = "0xde3d2d9dd52ea80f7799ef4791063a5458d13913" to_address = "0x056db290f8ba3250ca64a45d16284d04bc6f5fbf" value = 10000000000 nonce = 1048576 data = b'' gasprice = DEFAULT_GASPRICE startgas = DEFAULT_STARTGAS network_id = 1 tx1 = Transaction(nonce, gasprice, startgas, to_address, value, data, network_id, 0, 0) tx = encode_transaction(tx1) tx1.sign(data_decoder(sender_private_key), network_id=network_id) expected_signed_tx = encode_transaction(tx1) sig = data_encoder(signature_from_transaction(tx1)) signed_tx = add_signature_to_transaction(tx, sig) self.assertEqual(signed_tx, expected_signed_tx) tx_obj = decode_transaction(tx) add_signature_to_transaction(tx_obj, sig) self.assertEqual(tx_obj.network_id, network_id) self.assertEqual(data_encoder(tx_obj.sender), sender_address) self.assertEqual(encode_transaction(tx_obj), expected_signed_tx)
async def faucet(self, to, value, *, from_private_key=FAUCET_PRIVATE_KEY, startgas=None, gasprice=DEFAULT_GASPRICE, nonce=None, data=b"", wait_on_confirmation=True): if isinstance(from_private_key, str): from_private_key = data_decoder(from_private_key) from_address = private_key_to_address(from_private_key) ethclient = JsonRPCClient(config['ethereum']['url']) to = data_decoder(to) if len(to) not in (20, 0): raise Exception( 'Addresses must be 20 or 0 bytes long (len was {})'.format( len(to))) if nonce is None: nonce = await ethclient.eth_getTransactionCount(from_address) balance = await ethclient.eth_getBalance(from_address) if startgas is None: startgas = await ethclient.eth_estimateGas(from_address, to, data=data, nonce=nonce, value=value, gasprice=gasprice) tx = Transaction(nonce, gasprice, startgas, to, value, data, 0, 0, 0) if balance < (tx.value + (tx.startgas * tx.gasprice)): raise Exception("Faucet doesn't have enough funds") tx.sign(from_private_key) tx_encoded = data_encoder(rlp.encode(tx, Transaction)) tx_hash = await ethclient.eth_sendRawTransaction(tx_encoded) while wait_on_confirmation: resp = await ethclient.eth_getTransactionByHash(tx_hash) if resp is None or resp['blockNumber'] is None: await asyncio.sleep(0.1) else: break if to == b'': print("contract address: {}".format(data_encoder(tx.creates))) return tx_hash
async def deploy_contract(self, bytecode, *, from_private_key=FAUCET_PRIVATE_KEY, startgas=None, gasprice=DEFAULT_GASPRICE, wait_on_confirmation=True): if isinstance(from_private_key, str): from_private_key = data_decoder(from_private_key) from_address = private_key_to_address(from_private_key) ethclient = JsonRPCClient(config['ethereum']['url']) nonce = await ethclient.eth_getTransactionCount(from_address) balance = await ethclient.eth_getBalance(from_address) gasestimate = await ethclient.eth_estimateGas(from_address, '', data=bytecode, nonce=nonce, value=0, gasprice=gasprice) if startgas is None: startgas = gasestimate elif gasestimate > startgas: raise Exception( "Estimated gas usage is larger than the provided gas") tx = Transaction(nonce, gasprice, startgas, '', 0, bytecode, 0, 0, 0) if balance < (tx.value + (tx.startgas * tx.gasprice)): raise Exception("Faucet doesn't have enough funds") tx.sign(from_private_key) tx_encoded = data_encoder(rlp.encode(tx, Transaction)) tx_hash = await ethclient.eth_sendRawTransaction(tx_encoded) contract_address = data_encoder(tx.creates) while wait_on_confirmation: resp = await ethclient.eth_getTransactionByHash(tx_hash) if resp is None or resp['blockNumber'] is None: await asyncio.sleep(0.1) else: code = await ethclient.eth_getCode(contract_address) if code == '0x': raise Exception("Failed to deploy contract") break return tx_hash, contract_address
def test_sign_transaction_with_network_id(self): rlp = "0xec831000008504a817c80082520894056db290f8ba3250ca64a45d16284d04bc6f5fbf8502540be40080428080" tx = decode_transaction(rlp) tx = sign_transaction(tx, FAUCET_PRIVATE_KEY) self.assertEqual(data_encoder(tx.sender), FAUCET_ADDRESS) # after decoding a transaction, the rlp lib caches the rlp encoding from the given values # since the sign_transaction modifies that values, this makes sure the cache isn't used next # time the tx is encoded self.assertEqual( data_encoder(tx.hash), "0x102234ef30bb90955517ddf92dc7ce39fff7bb4ef760409b984983ad73585bf5", "Incorrect transaction hash after signing")
def test_tx_network_id(self): network_id = 1 tx = create_transaction( nonce=9, gasprice=20 * 10**9, startgas=21000, to="0x3535353535353535353535353535353535353535", value=10**18, data=b'', network_id=network_id) key = data_decoder( "0x4646464646464646464646464646464646464646464646464646464646464646" ) self.assertEqual( "0xec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080", encode_transaction(tx)) tx.sign(key, network_id=network_id) self.assertEqual( "0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83", data_encoder(rlp.encode(tx))) self.assertEqual(( 37, 0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276, 0x67cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83 ), (tx.v, tx.r, tx.s))
def do_POST(self): # TODO: figure out why read is blocking here data = self.rfile.read(len(self.rfile.peek())) data = data.decode('utf-8') data = json.loads(data) if self.path == "/v1/tx/skel": gas_price = parse_int(data['gas_price']) if 'gas_price' in data else DEFAULT_GASPRICE gas = parse_int(data['gas']) if 'gas' in data else DEFAULT_STARTGAS nonce = parse_int(data['nonce']) if 'nonce' in data else 0 if 'value' not in data or 'from' not in data or 'to' not in data: self.write_data(400, {'errors': [{'id': 'bad_arguments', 'message': 'Bad Arguments'}]}) return value = parse_int(data['value']) to_address = data['to'] from_address = data['from'] if not validate_address(to_address): self.write_data(400, {'errors': [{'id': 'invalid_to_address', 'message': 'Invalid To Address'}]}) return if not validate_address(from_address): self.write_data(400, {'errors': [{'id': 'invalid_from_address', 'message': 'Invalid From Address'}]}) return tx = create_transaction(nonce=nonce, gasprice=gas_price, startgas=gas, to=to_address, value=value) transaction = encode_transaction(tx) self.write_data(200, { "tx_data": { "nonce": hex(nonce), "from": from_address, "to": to_address, "value": hex(value), "startGas": hex(gas), "gasPrice": hex(gas_price) }, "tx": transaction }) elif self.path == "/v1/tx": tx = decode_transaction(data['tx']) if 'signature' in data: sig = data_decoder(data['signature']) add_signature_to_transaction(tx, sig) self.write_data(200, {"tx_hash": data_encoder(tx.hash)}) else: self.write_data(404)
def test_encode_decode_transaction(self): sender_private_key = "0x0164f7c7399f4bb1eafeaae699ebbb12050bc6a50b2836b9ca766068a9d000c0" sender_address = "0xde3d2d9dd52ea80f7799ef4791063a5458d13913" to_address = "0x056db290f8ba3250ca64a45d16284d04bc6f5fbf" value = 10000000000 nonce = 1048576 data = b'' gasprice = 20000000000 startgas = DEFAULT_STARTGAS expected_tx_hash = "0x2f321aa116146a9bc62b61c76508295f708f42d56340c9e613ebfc27e33f240c" tx1 = Transaction(nonce, gasprice, startgas, to_address, value, data) tx1.sign(data_decoder(sender_private_key)) self.assertEqual(data_encoder(tx1.hash), expected_tx_hash) # rlputx1 = rlp.encode(tx1, UnsignedTransaction) # rlpstx1 = rlp.encode(tx1, Transaction) tx1 = Transaction(nonce, gasprice, startgas, to_address, value, data) enc1 = rlp.encode(tx1, UnsignedTransaction) tx2 = rlp.decode(enc1, UnsignedTransaction) tx2.sign(data_decoder(sender_private_key)) tx3 = Transaction(tx2.nonce, tx2.gasprice, tx2.startgas, tx2.to, tx2.value, tx2.data) tx3.sign(data_decoder(sender_private_key)) self.assertEqual(data_encoder(tx3.sender), sender_address) self.assertEqual(data_encoder(tx3.hash), expected_tx_hash) self.assertEqual(data_encoder(tx2.sender), sender_address) # NOTE: this is false because tx2 still thinks it's an unsigned tx # so it doesn't include the signature variables in the tx # if this suddenly starts failing, it means the behaviour # has been modified in the library self.assertNotEqual(data_encoder(tx2.hash), expected_tx_hash)
def test_vector(name, vector): if 'transaction' not in vector: return # TODO: process rlp tests transaction = vector['transaction'] tx = create_transaction( nonce=parse_int(transaction['nonce']), gasprice=parse_int(transaction['gasPrice']), startgas=parse_int(transaction['gasLimit']), to=transaction['to'], value=parse_int(transaction['value']), data=data_decoder(transaction['data']), v=parse_int(transaction['v']), r=parse_int(transaction['r']), s=parse_int(transaction['s'])) self.assertEqual(data_encoder(tx.sender), "0x{}".format(vector['sender']), name) self.assertEqual(calculate_transaction_hash(tx), "0x{}".format(vector['hash']), name) self.assertEqual(encode_transaction(tx), vector['rlp'], name) # test decode transaction -> encode transaction round trip tx = decode_transaction(vector['rlp']) self.assertEqual(encode_transaction(tx), vector['rlp'], name)
async def __call__(self, *args, startgas=None, gasprice=20000000000, value=0, wait_for_confirmation=True): # TODO: figure out if we can validate args validated_args = [] for (type, name), arg in zip( self.contract.translator.function_data[self.name]['signature'], args): if type == 'address' and isinstance(arg, str): validated_args.append(data_decoder(arg)) elif (type.startswith("uint") or type.startswith("int")) and isinstance(arg, str): validated_args.append(int(arg, 16)) else: validated_args.append(arg) ethurl = get_url() ethclient = JsonRPCClient(ethurl) data = self.contract.translator.encode_function_call( self.name, validated_args) # TODO: figure out if there's a better way to tell if the function needs to be called via sendTransaction if self.is_constant: result = await ethclient.eth_call(from_address=self.from_address or '', to_address=self.contract.address, data=data) result = data_decoder(result) if result: decoded = self.contract.translator.decode_function_result( self.name, result) # make sure addresses are encoded as expected decoded = fix_address_decoding( decoded, self.contract.translator.function_data[self.name] ['decode_types']) # return the single value if there is only a single return value if len(decoded) == 1: return decoded[0] return decoded return None else: if self.from_address is None: raise Exception( "Cannot call non-constant function without a sender") nonce = await ethclient.eth_getTransactionCount(self.from_address) balance = await ethclient.eth_getBalance(self.from_address) if startgas is None: startgas = await ethclient.eth_estimateGas( self.from_address, self.contract.address, data=data, nonce=nonce, value=value, gasprice=gasprice) if startgas == 50000000 or startgas is None: raise Exception( "Unable to estimate gas cost, possibly something wrong with the transaction arguments" ) if balance < (startgas * gasprice): raise Exception("Given account doesn't have enough funds") tx = Transaction(nonce, gasprice, startgas, self.contract.address, value, data, 0, 0, 0) tx.sign(self.from_key) tx_encoded = data_encoder(rlp.encode(tx, Transaction)) if self.return_raw_tx: return tx_encoded try: tx_hash = await ethclient.eth_sendRawTransaction(tx_encoded) except: print(balance, startgas * gasprice, startgas) raise # wait for the contract to be deployed if wait_for_confirmation: print("waiting on transaction: {}".format(tx_hash)) starttime = time.time() warnlevel = 0 while wait_for_confirmation: resp = await ethclient.eth_getTransactionByHash(tx_hash) if resp is None or resp['blockNumber'] is None: await asyncio.sleep(0.1) if resp is None and warnlevel == 0 and time.time( ) - starttime < 10: print( "WARNING: 10 seconds have passed and transaction is not showing as a pending transaction" ) warnlevel = 1 elif resp is None and warnlevel == 1 and time.time( ) - starttime < 60: print( "WARNING: 60 seconds have passed and transaction is not showing as a pending transaction" ) raise Exception( "Unexpected error waiting for transaction to complete" ) else: receipt = await ethclient.eth_getTransactionReceipt(tx_hash ) if 'status' in receipt and receipt['status'] != "0x1": raise Exception( "Transaction status returned {}".format( receipt['status'])) break # TODO: is it possible for non-const functions to have return types? return tx_hash
async def from_source_code(cls, sourcecode, contract_name, constructor_data=None, *, address=None, deployer_private_key=None, import_mappings=None, libraries=None, optimize=False, deploy=True, cwd=None, wait_for_confirmation=True): if deploy: ethurl = get_url() if address is None and deployer_private_key is None: raise TypeError( "requires either address or deployer_private_key") if address is None and not isinstance(constructor_data, (list, type(None))): raise TypeError( "must supply constructor_data as a list (hint: use [] if args should be empty)" ) args = ['solc', '--combined-json', 'bin,abi'] if libraries: args.extend([ '--libraries', ','.join(['{}:{}'.format(*library) for library in libraries]) ]) if optimize: args.append('--optimize') if import_mappings: args.extend([ "{}={}".format(path, mapping) for path, mapping in import_mappings ]) # check if sourcecode is actually a filename if cwd: filename = os.path.join(cwd, sourcecode) else: filename = sourcecode if os.path.exists(filename): args.append(filename) sourcecode = None else: filename = '<stdin>' process = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd) output, stderrdata = process.communicate(input=sourcecode) try: output = json_decode(output) except json.JSONDecodeError: if output and stderrdata: output += b'\n' + stderrdata elif stderrdata: output = stderrdata raise Exception("Failed to compile source: {}\n{}\n{}".format( filename, ' '.join(args), output.decode('utf-8'))) try: contract = output['contracts']['{}:{}'.format( filename, contract_name)] except KeyError: print(output) raise abi = json_decode(contract['abi']) # deploy contract translator = ContractTranslator(abi) # fix things that don't have a constructor if not deploy: return Contract(abi=abi, address=address, translator=translator) ethclient = JsonRPCClient(ethurl) if address is not None: # verify there is code at the given address for i in range(10): code = await ethclient.eth_getCode(address) if code == "0x": await asyncio.sleep(1) continue break else: raise Exception("No code found at given address") return Contract(abi=abi, address=address, translator=translator) try: bytecode = data_decoder(contract['bin']) except binascii.Error: print(contract['bin']) raise if constructor_data is not None: constructor_call = translator.encode_constructor_arguments( constructor_data) bytecode += constructor_call if isinstance(deployer_private_key, str): deployer_private_key = data_decoder(deployer_private_key) deployer_address = private_key_to_address(deployer_private_key) nonce = await ethclient.eth_getTransactionCount(deployer_address) balance = await ethclient.eth_getBalance(deployer_address) gasprice = 20000000000 value = 0 startgas = await ethclient.eth_estimateGas(deployer_address, '', data=bytecode, nonce=nonce, value=0, gasprice=gasprice) if balance < (startgas * gasprice): raise Exception("Given account doesn't have enough funds") tx = Transaction(nonce, gasprice, startgas, '', value, bytecode, 0, 0, 0) tx.sign(deployer_private_key) tx_encoded = data_encoder(rlp.encode(tx, Transaction)) contract_address = data_encoder(tx.creates) tx_hash = await ethclient.eth_sendRawTransaction(tx_encoded) # wait for the contract to be deployed while wait_for_confirmation: resp = await ethclient.eth_getTransactionByHash(tx_hash) if resp is None or resp['blockNumber'] is None: await asyncio.sleep(0.1) else: code = await ethclient.eth_getCode(contract_address) if code == '0x': raise Exception( "Failed to deploy contract: resulting address '{}' has no code" .format(contract_address)) break return Contract(abi=abi, address=contract_address, translator=translator, creation_tx_hash=tx_hash)