def publish(self, raw_transaction: str) -> Optional[str]: ''' Method for publishing transactions. Args: raw_transaction (str): hex string containing signed transaction Returns: str, None: transaction address or None if transaction wasn't published Example: >>> raw_transaction = '010000000184a38a4e8743964249665fb241fbd3...35b' >>> network.publish(raw_transaction) 70eefae0106b787e592e12914e4040efd8181dd299fa314d8f66da6a95cd1cfe ''' for attempt in range(1, TRANSACTION_BROADCASTING_MAX_ATTEMPTS + 1): transaction_address = self.broadcast_transaction(raw_transaction) if transaction_address is None: logger.warning( 'Transaction broadcast attempt no. %s failed. Retrying...', attempt) continue logger.info( 'Transaction broadcast is successful. End of broadcasting process.' ) return transaction_address logger.warning( '%s attempts to broadcast transaction failed. Broadcasting process terminates!', TRANSACTION_BROADCASTING_MAX_ATTEMPTS)
def get_token_by_symbol(cls, symbol: str): """ Get raw_transaction by encoding Transaction object token by provided symbol """ token = cls.get_token_by_attribute('symbol', symbol) if not token: logger.warning(f'No token found for symbol {symbol}') return return cls.token_class.from_namedtuple(token)
def sign_raw_transaction(cls, raw_transaction: str, private_key: str) -> str: ''' Method to sign raw transactions. Args: raw_transaction (str): raw transaction hex string private_key (str): private key hex string Returns: str: signed transaction hex string Raises: ValueError: if given private key is invalid Example: >>> from clove.network import EthereumTestnet >>> network = EthereumTestnet() >>> raw_transaction = '0xf8f28201f4843b9aca008302251694ce07ab9477bc20790b88b398a2a9e0f626c7d26387b1a2bc2ec50000b8c47337c993000000000000000000000000000000000000000000000000000000005bd564819d3e84874c199ca4656d434060ec1a393750ab74000000000000000000000000000000000000000000000000d867f293ba129629a9f9355fa285b8d3711a9092000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000808080' # noqa: E501 >>> network.sign_raw_transaction(raw_transaction, MY_PRIVATE_KEY) '0xf901318201f4843b9aca008302251694ce07ab9477bc20790b88b398a2a9e0f626c7d26387b1a2bc2ec50000b8c47337c993000000000000000000000000000000000000000000000000000000005bd564819d3e84874c199ca4656d434060ec1a393750ab74000000000000000000000000000000000000000000000000d867f293ba129629a9f9355fa285b8d3711a90920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001ca0d1c5b984ef2629eeb7c96f48a645566b2caf4130b0f3d7060ad5225946eee9e99f9928c5dfe868b45efbb9f8ae7d64d6162591c78961439c49e836947842e178' # noqa: E501 ''' transaction = cls.deserialize_raw_transaction(raw_transaction) try: transaction.sign(private_key) logger.debug("Transaction signed") except Exception: logger.warning( "Invalid private key. Transaction could not be signed.") raise ValueError('Invalid private key.') return cls.get_raw_transaction(transaction)
def approve_token( self, sender_address: str, value: Union[str, Decimal], token_address: str=None, ) -> EthereumTokenApprovalTransaction: if not isinstance(value, Decimal): value = Decimal(str(value)) token = None if token_address: token = self.get_token_by_address(token_address) if not token: logger.warning('Unknown ethereum token') raise ValueError('Unknown token') transaction = EthereumTokenApprovalTransaction( self, sender_address, value, token, ) return transaction
def deserialize_raw_transaction(raw_transaction: str) -> Transaction: ''' Deserializing raw transaction and returning Transaction object Args: raw_transaction (str): raw transaction hex string Returns: `ethereum.transactions.Transaction`: Ethereum transaction object Raises: ImpossibleDeserialization: if the raw transaction was not deserializable Example: >>> from clove.network import EthereumTestnet >>> network = EthereumTestnet() >>> transaction = network.deserialize_raw_transaction('0xf8f28201f4843b9aca008302251694ce07ab9477bc20790b88b398a2a9e0f626c7d26387b1a2bc2ec50000b8c47337c993000000000000000000000000000000000000000000000000000000005bd564819d3e84874c199ca4656d434060ec1a393750ab74000000000000000000000000000000000000000000000000d867f293ba129629a9f9355fa285b8d3711a9092000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000808080') # noqa: E501 <Transaction(821b)> ''' try: transaction = rlp.hex_decode(raw_transaction, Transaction) logger.debug('Deserialization succeed') except (ValueError, RLPException): logger.warning(f'Deserialization with {raw_transaction} failed') raise ImpossibleDeserialization() transaction._cached_rlp = None transaction.make_mutable() return transaction
def publish(self, transaction: Union[str, Transaction]) -> Optional[str]: ''' Method to publish transaction Args: transaction (str, `ethereum.transactions.Transaction`): signed transaction Returns: str, None: transaction hash or None if something goes wrong Example: >>> from clove.network import EthereumTestnet >>> network = EthereumTestnet() >>> signed_transaction = '0xf901318201f4843b9aca008302251694ce07ab9477bc20790b88b398a2a9e0f626c7d26387b1a2bc2ec50000b8c47337c993000000000000000000000000000000000000000000000000000000005bd564819d3e84874c199ca4656d434060ec1a393750ab74000000000000000000000000000000000000000000000000d867f293ba129629a9f9355fa285b8d3711a90920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001ca0d1c5b984ef2629eeb7c96f48a645566b2caf4130b0f3d7060ad5225946eee9e99f9928c5dfe868b45efbb9f8ae7d64d6162591c78961439c49e836947842e178' # noqa: E501 >>> network.publish(signed_transaction) '0x4fd41289b816f6122e59a0759bd10441ead75d550562f4b3aad2fddc56eb3274' ''' raw_transaction = transaction if isinstance( transaction, str) else self.get_raw_transaction(transaction) try: published_transaction = self.web3.eth.sendRawTransaction( raw_transaction).hex() logger.debug( f'Transaction {published_transaction} published successful') return published_transaction except ValueError: logger.warning(f'Unable to publish transaction {raw_transaction}') return
def get_token_by_address(self, address: str): token = self.get_token_by_attribute( 'address', address) or self.get_token_from_token_contract(address) if not token: logger.warning(f'No token found for address {address}') return return EthToken.from_namedtuple(token)
def get_fee(cls) -> Optional[float]: ''' Getting actual fee per kb Returns: float, None: actual fee per kb or None if eg. API is not responding Example: >>> from clove.network import BitcoinTestNet >>> network = BitcoinTestNet() >>> network.get_fee() 0.00024538 ''' try: # This endpoint is available from v0.3.1 fee = clove_req_json( f'{cls.api_url}/utils/estimatefee?nbBlocks=1')['1'] except (TypeError, KeyError): logger.error( f'Incorrect response from API when getting fee from {cls.api_url}/utils/estimatefee?nbBlocks=1' ) return cls._calculate_fee() if fee == -1: logger.debug(f'Incorrect value in estimatedFee: {fee}') return cls._calculate_fee() fee = float(fee) if fee > 0: logger.warning( f'Got fee = 0 for ({cls.symbols[0]}), calculating manually') return fee return cls._calculate_fee()
def get_method_name(self, method_id: str) -> str: ''' Returning Atomic Swap method name based on method identifier. Args: method_id (str): method identifier Returns: str: method name Raises: UnsupportedTransactionType: if method identifier is not recognized Example: >>> from clove.network import EthereumTestnet >>> network = EthereumTestnet() >>> network.get_method_name('7337c993') 'initiate' ''' try: return { self.initiate: 'initiate', self.redeem: 'redeem', self.refund: 'refund', }[method_id] except KeyError: logger.warning(f'Unrecognized method id {self.method_id}') raise UnsupportedTransactionType( f'Unrecognized method id {self.method_id}')
def refund(self): contract = self.network.web3.eth.contract(address=self.contract_address, abi=self.network.abi) if self.locktime > datetime.utcnow(): locktime_string = self.locktime.strftime('%Y-%m-%d %H:%M:%S') logger.warning(f"This contract is still valid! It can't be refunded until {locktime_string} UTC.") raise RuntimeError(f"This contract is still valid! It can't be refunded until {locktime_string} UTC.") refund_func = contract.functions.refund(self.secret_hash, self.recipient_address) tx_dict = { 'nonce': self.network.web3.eth.getTransactionCount(self.refund_address), 'value': 0, 'gas': ETH_REFUND_GAS_LIMIT, } tx_dict = refund_func.buildTransaction(tx_dict) transaction = EthereumTokenTransaction(network=self.network) transaction.tx = Transaction( nonce=tx_dict['nonce'], gasprice=tx_dict['gasPrice'], startgas=tx_dict['gas'], to=tx_dict['to'], value=tx_dict['value'], data=Web3.toBytes(hexstr=tx_dict['data']), ) transaction.value = self.value transaction.token = self.token transaction.recipient_address = self.refund_address logger.debug('Transaction refunded') return transaction
def atomic_swap( self, sender_address: str, recipient_address: str, value: Union[str, Decimal], secret_hash: str=None, token_address: str=None, ) -> EthereumAtomicSwapTransaction: """ Return EthereumAtomicSwapTransaction object, which initiate and build transaction beetwen sender and recipient """ if not isinstance(value, Decimal): value = Decimal(str(value)) token = None if token_address: token = self.get_token_by_address(token_address) if not token: logger.warning('Unknown ethereum token') raise ValueError('Unknown token') transaction = EthereumAtomicSwapTransaction( self, sender_address, recipient_address, value, secret_hash, token, ) return transaction
def publish(self, transaction: Union[str, Transaction]) -> Optional[str]: """ Method to publish transaction """ raw_transaction = transaction if isinstance(transaction, str) else self.get_raw_transaction(transaction) try: published_transaction = self.web3.eth.sendRawTransaction(raw_transaction).hex() logger.debug(f'Transaction {published_transaction} published successful') return published_transaction except ValueError: logger.warning(f'Unable to publish transaction {raw_transaction}') return
def get_method_name(self, method_id): try: return { self.initiate: 'initiate', self.initiate_token: 'initiate', self.redeem: 'redeem', self.refund: 'refund', }[method_id] except KeyError: logger.warning(f'Unrecognized method id {self.method_id}') raise UnsupportedTransactionType(f'Unrecognized method id {self.method_id}')
def sign_raw_transaction(cls, raw_transaction: str, private_key: str) -> str: """ Method to sign raw transactions """ transaction = cls.deserialize_raw_transaction(raw_transaction) try: transaction.sign(private_key) logger.debug("Transaction signed") except Exception: logger.warning("Invalid private key. Transaction could not be signed.") raise ValueError('Invalid private key.') return cls.get_raw_transaction(transaction)
def web3_provider_address(self) -> str: ''' Returns: str: address for web3 provider. Raises: ValueError: if the `INFURA_TOKEN` environment variable is not set. ''' token = os.environ.get('INFURA_TOKEN') if not token: logger.warning('INFURA_TOKEN environment variable was not set.') raise ValueError('INFURA_TOKEN environment variable was not set.') return f'https://{self.infura_network}.infura.io/{token}'
def get_fee(cls) -> float: """Ravencoin has a different endpoint for fee (estimatesmartfee, not estimatefee)""" try: fee = clove_req_json(f'{cls.api_url}/utils/estimatesmartfee?nbBlocks=1')['1'] except (TypeError, KeyError): logger.error( f'Incorrect response from API when getting fee from {cls.api_url}/utils/estimatefee?nbBlocks=1' ) return cls._calculate_fee() if fee > 0: return fee logger.warning(f'({cls.symbols[0]}) Got fee = 0, calculating manually') return cls._calculate_fee()
def atomic_swap( self, sender_address: str, recipient_address: str, value: Union[str, Decimal], secret_hash: str = None, token_address: str = None, ) -> EthereumAtomicSwapTransaction: ''' Return EthereumAtomicSwapTransaction object, which initiate and build transaction between sender and recipient. Args: sender_address (str): wallet address of the sender recipient_address (str): wallet address of the recipient value (str, Decimal): amount to swap secret_hash (str): optional secret hash to be used in transaction. If None then the new hash will be generated. token_address: address of the ERC20 token contract to swap Returns: EthereumAtomicSwapTransaction: atomic swap unsigned transaction for Ethereum Raises: ValueError: if you use an incorrect token address Example: >>> from clove.network import EthereumTestnet >>> network = EthereumTestnet() >>> network.atomic_swap('0x999F348959E611F1E9eab2927c21E88E48e6Ef45', '0xd867f293Ba129629a9f9355fa285B8D3711a9092', '0.05') # noqa: E501 <clove.network.ethereum.transaction.EthereumAtomicSwapTransaction at 0x7f286d16dba8> ''' if not isinstance(value, Decimal): value = Decimal(str(value)) token = None if token_address: token = self.get_token_by_address(token_address) if not token: logger.warning('Unknown ethereum token') raise ValueError('Unknown token') transaction = EthereumAtomicSwapTransaction( self, sender_address, recipient_address, value, secret_hash, token, ) return transaction
def clove_req(url: str) -> Optional[HTTPResponse]: """Make a request with Clove user-agent header""" req = urllib.request.Request(url, headers={'User-Agent': 'Clove'}) try: request_start = time.time() logger.debug(' Requesting: %s', url) resp = urllib.request.urlopen(req) response_time = time.time() - request_start logger.debug('Got response: %s [%.2fs]', url, response_time) except (HTTPError, URLError) as e: logger.warning('Could not open url %s', url) logger.exception(e) return return resp
def deserialize_raw_transaction(raw_transaction: str) -> Optional[Transaction]: """ Method to deserialize raw method. It's deserializing raw_transaction and returns Transaction object""" try: transaction = rlp.hex_decode(raw_transaction, Transaction) logger.debug('Deserialization succeed') except (ValueError, RLPException): logger.warning(f'Deserialization with {raw_transaction} failed') raise ImpossibleDeserialization() transaction._cached_rlp = None transaction.make_mutable() return transaction
def refund(self) -> EthereumTokenTransaction: ''' Creates transaction that can refund a contract. Returns: EthereumTokenTransaction: unsigned transaction object with refund transaction Raises: RuntimeError: if contract is still valid ValueError: if contract balance is 0 ''' if self.balance == 0: raise ValueError("Balance of this contract is 0.") contract = self.contract if self.locktime > datetime.utcnow(): locktime_string = self.locktime.strftime('%Y-%m-%d %H:%M:%S') logger.warning( f"This contract is still valid! It can't be refunded until {locktime_string} UTC." ) raise RuntimeError( f"This contract is still valid! It can't be refunded until {locktime_string} UTC." ) refund_func = contract.functions.refund(self.secret_hash, self.recipient_address) tx_dict = { 'nonce': self.network.web3.eth.getTransactionCount(self.refund_address), 'value': 0, 'gas': ETH_REFUND_GAS_LIMIT, } tx_dict = refund_func.buildTransaction(tx_dict) transaction = EthereumTokenTransaction(network=self.network) transaction.tx = Transaction( nonce=tx_dict['nonce'], gasprice=tx_dict['gasPrice'], startgas=tx_dict['gas'], to=tx_dict['to'], value=tx_dict['value'], data=Web3.toBytes(hexstr=tx_dict['data']), ) transaction.value = self.value transaction.token = self.token transaction.recipient_address = self.refund_address logger.debug('Transaction refunded') return transaction
def get_token_from_token_contract(self, token_address: str) -> Optional[Token]: """ Getting information from token contract and creating Token. Smart contract is taken based on provided address """ token_address = self.unify_address(token_address) token_contract = self.web3.eth.contract(address=token_address, abi=ERC20_BASIC_ABI) concise = ConciseContract(token_contract) try: name = concise.name() symbol = concise.symbol() decimals = concise.decimals() logger.debug(f'Token get from contract with success') except (OverflowError, BadFunctionCallOutput): logger.warning(f'Unable to take token from address: {token_address}') return return Token(name, symbol, token_address, decimals)
def update_blacklist(self, node): ''' Increasing number of failed connection attempts to a give node. Args: node (str): node's IP address or domain Returns: None ''' try: self.blacklist_nodes[node] += 1 except KeyError: logger.warning('Unable to update blacklist') self.blacklist_nodes[node] = 1
def __init__(self, network, tx_dict): self.network = network self.tx_dict = tx_dict self.method_id = self.network.extract_method_id(tx_dict['input']) self.type = self.network.get_method_name(self.method_id) self.token = None if not self.is_initiate: logger.warning('Not a contract transaction.') raise ValueError('Not a contract transaction.') if self.is_token_contract: self.abi = self.network.token_abi else: self.abi = self.network.abi input_types = get_abi_input_types( find_matching_fn_abi(self.abi, fn_identifier=self.type)) input_names = get_abi_input_names( find_matching_fn_abi(self.abi, fn_identifier=self.type)) input_values = decode_abi( input_types, Web3.toBytes(hexstr=self.tx_dict['input'][10:])) self.inputs = dict(zip(input_names, input_values)) self.locktime = datetime.utcfromtimestamp(self.inputs['_expiration']) self.recipient_address = Web3.toChecksumAddress( self.inputs['_participant']) self.refund_address = self.tx_dict['from'] self.secret_hash = self.inputs['_hash'].hex() self.contract_address = Web3.toChecksumAddress(self.tx_dict['to']) self.confirmations = self.network.latest_block - self.tx_dict[ 'blockNumber'] if self.is_token_contract: self.value_base_units = self.inputs['_value'] self.token_address = Web3.toChecksumAddress(self.inputs['_token']) self.token = self.network.get_token_by_address(self.token_address) self.value = self.token.value_from_base_units( self.value_base_units) self.symbol = self.token.symbol else: self.value_base_units = self.tx_dict['value'] self.value = self.network.value_from_base_units( self.value_base_units) self.symbol = self.network.default_symbol
def publish(self, raw_transaction: str): for attempt in range(1, TRANSACTION_BROADCASTING_MAX_ATTEMPTS + 1): transaction_address = self.broadcast_transaction(raw_transaction) if transaction_address is None: logger.warning( 'Transaction broadcast attempt no. %s failed. Retrying...', attempt) continue logger.info( 'Transaction broadcast is successful. End of broadcasting process.' ) return transaction_address logger.warning( '%s attempts to broadcast transaction failed. Broadcasting process terminates!', TRANSACTION_BROADCASTING_MAX_ATTEMPTS)
def get_fee(cls) -> Optional[float]: # This endpoint is available from v0.3.1 try: fee = clove_req_json(f'{cls.api_url}/utils/estimatefee?nbBlocks=1')['1'] except (TypeError, KeyError): logger.error( f'Incorrect response from API when getting fee from {cls.api_url}/utils/estimatefee?nbBlocks=1' ) return cls._calculate_fee() if fee == -1: logger.debug(f'Incorrect value in estimatedFee: {fee}') return cls._calculate_fee() fee = float(fee) if fee > 0: logger.warning(f'Got fee = 0 for ({cls.symbols[0]}), calculating manually') return fee return cls._calculate_fee()
def approve_token( self, sender_address: str, value: Union[str, Decimal], token_address: str = None, ) -> EthereumTokenApprovalTransaction: ''' Create unsigned token approve transaction. Args: sender_address (str): wallet address of the sender value (str, Decimal): amount to swap token_address: address of the ERC20 token contract to swap Returns: EthereumTokenApprovalTransaction: unsigned token approve transaction Raises: ValueError: if you use an incorrect token address Example: >>> from clove.network import EthereumTestnet >>> network = EthereumTestnet() >>> network.approve_token('0x999F348959E611F1E9eab2927c21E88E48e6Ef45', '0.05', '0x53E546387A0d054e7FF127923254c0a679DA6DBf') # noqa: E501 <clove.network.ethereum.transaction.EthereumTokenApprovalTransaction at 0x7f286d14bc50> ''' if not isinstance(value, Decimal): value = Decimal(str(value)) token = None if token_address: token = self.get_token_by_address(token_address) if not token: logger.warning('Unknown ethereum token') raise ValueError('Unknown token') transaction = EthereumTokenApprovalTransaction( self, sender_address, value, token, ) return transaction
def get_token_by_symbol(cls, symbol: str) -> Optional[EthToken]: ''' Get a known token (from clove) by its symbol. Args: symbol (str): token symbol Returns: EthToken, None: Ethereum Token namedtuple or None if there is no matching token Example: >>> from clove.network import EthereumTestnet >>> network = EthereumTestnet() >>> network.get_token_by_symbol('PGT') <clove.network.ethereum.token.EthToken at 0x7f7b3fdffe48> ''' token = cls.get_token_by_attribute('symbol', symbol) if not token: logger.warning(f'No token found for symbol {symbol}') return return EthToken.from_namedtuple(token)
def get_token_by_address(self, address: str) -> Optional[EthToken]: ''' Get token by its address. Args: address (str): token address Returns: EthToken, None: Ethereum Token namedtuple Example: >>> from clove.network import EthereumTestnet >>> network = EthereumTestnet() >>> network.get_token_by_address('0x2c76B98079Bb5520FF4BDBC1bf5012AC3E87ddF6') <clove.network.ethereum.token.EthToken at 0x7f7b3fed1eb8> ''' token = self.get_token_by_attribute( 'address', address) or self.get_token_from_token_contract(address) if not token: logger.warning(f'No token found for address {address}') return return EthToken.from_namedtuple(token)
def extract_secret_from_redeem_transaction(cls, contract_address: str) -> Optional[str]: api_key = os.environ.get('CRYPTOID_API_KEY') if not api_key: raise ValueError('API key for cryptoid is required.') data = clove_req_json(f'{cls.cryptoid_url()}/api.dws?q=multiaddr&active={contract_address}&key={api_key}') if not data: logger.debug('Unexpected response from cryptoid') raise ValueError('Unexpected response from cryptoid') transactions = data['txs'] if len(transactions) == 1: logger.debug('Contract was not redeemed yet.') return redeem_tx_hash = transactions[0]['hash'] logger.warning('Using undocumented endpoint used by chainz.cryptoid.info site.') data = clove_req_json(f'{cls.api_url}/explorer/tx.raw.dws?coin={cls.symbols[0].lower()}&id={redeem_tx_hash}') if not data: logger.debug('Unexpected response from cryptoid') raise ValueError('Unexpected response from cryptoid') return cls.extract_secret(scriptsig=data['vin'][0]['scriptSig']['hex'])
def get_token_from_token_contract(self, token_address: str) -> Optional[Token]: ''' Getting information from token contract (remote) Args: token_address (str): address of the token contract Returns: Token, None: Ethereum Token namedtuple or None if there is something goes wrong Raises: RuntimeError: if name or symbol of the token is not defined. Example: >>> from clove.network import EthereumTestnet >>> network = EthereumTestnet() >>> network.get_token_from_token_contract('0x2c76B98079Bb5520FF4BDBC1bf5012AC3E87ddF6') Token(name='PrettyGoodToken', symbol='PGT', address='0x2c76B98079Bb5520FF4BDBC1bf5012AC3E87ddF6', decimals=18) # noqa: E501 ''' token_address = self.unify_address(token_address) token_contract = self.web3.eth.contract(address=token_address, abi=ERC20_BASIC_ABI) concise = ConciseContract(token_contract) try: name = concise.name() symbol = concise.symbol() decimals = concise.decimals() if name == '' or symbol == '': raise RuntimeError( 'Unable to extract token details from token contract') logger.debug(f'Token get from contract with success') except (OverflowError, BadFunctionCallOutput): logger.warning( f'Unable to take token from address: {token_address}') return return Token(name, symbol, token_address, decimals)