def get_utxo(cls, address: str, amount: float): api_key = os.environ.get('CRYPTOID_API_KEY') if not api_key: raise ValueError('API key for cryptoid is required to get UTXOs.') data = clove_req_json(f'{cls.cryptoid_url()}/api.dws?q=unspent&key={api_key}&active={address}') unspent = data.get('unspent_outputs', []) for output in unspent: output['value'] = int(output['value']) unspent = sorted(unspent, key=lambda k: k['value'], reverse=True) utxo = [] total = 0 for output in unspent: value = from_base_units(output['value']) utxo.append( Utxo( tx_id=output['tx_hash'], vout=output['tx_ouput_n'], value=value, tx_script=output['script'], ) ) total += value if total > amount: return utxo logger.debug(f'Cannot find enough UTXO\'s. Found %.8f from %.8f.', total, amount)
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 extract_secret_from_redeem_transaction(self, tx_address: str) -> str: ''' Extracting secret from redeem transaction. Args: tx_address (str): address of the redeem transaction Returns: str,: Secret string Raises: ValueError: When given transaction was not a redeem type transaction Example: >>> from clove.network import EthereumTestnet >>> network = EthereumTestnet() >>> network.extract_secret_from_redeem_transaction('0x9e41847c3cc780e4cb59902cf55657f0ee92642d9dee4145e090cbf206d4748f') # noqa: E501 b2eefaadbbefeb9d9467092b612464db7c6724f71b5c1d70c85853845728f0e9 ''' tx_dict = self.get_transaction(tx_address) method_id = self.extract_method_id(tx_dict['input']) if method_id != self.redeem: logger.debug('Not a redeem transaction.') raise ValueError('Not a redeem transaction.') method_name = self.get_method_name(method_id) input_types = get_abi_input_types( find_matching_fn_abi(self.abi, fn_identifier=method_name)) input_values = decode_abi(input_types, Web3.toBytes(hexstr=tx_dict['input'][10:])) return input_values[0].hex()
def find_redeem_transaction( recipient_address: str, contract_address: str, value: int, subdomain: str, ) -> Optional[str]: recipient_address = recipient_address.lower() contract_address = contract_address.lower() value = str(value) etherscan_api_key = os.getenv('ETHERSCAN_API_KEY') if not etherscan_api_key: raise ValueError('API key for etherscan is required.') data = clove_req_json( f'http://{subdomain}.etherscan.io/api?module=account&action=txlistinternal' f'&address={recipient_address}&apikey={etherscan_api_key}') for result in reversed(data['result']): if result['to'] == recipient_address and result[ 'from'] == contract_address and result['value'] == value: return result['hash'] logger.debug('Redeem transaction not found.')
def get_utxo(cls, address: str, amount: float): data = clove_req_json( f'{cls.blockcypher_url()}/addrs/{address}' '?limit=2000&unspentOnly=true&includeScript=true&confirmations=6') unspent = data.get('txrefs', []) for output in unspent: output['value'] = int(output['value']) unspent = sorted(unspent, key=lambda k: k['value'], reverse=True) utxo = [] total = 0 for output in unspent: value = from_base_units(output['value']) utxo.append( Utxo( tx_id=output['tx_hash'], vout=output['tx_output_n'], value=value, tx_script=output['script'], )) total += value if total > amount: return utxo logger.debug(f'Cannot find enough UTXO\'s. Found %.8f from %.8f.', total, amount)
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 get_latest_block(cls) -> Optional[int]: ''' Returns the number of the latest block. Returns: int, None: number of the latest block or None if API is not working Example: >>> from clove.network import Bitcoin >>> network = Bitcoin() >>> network.get_latest_block() 544989 ''' try: latest_block = clove_req_json( f'{cls.api_url}/status?q=getInfo')['info']['blocks'] except (TypeError, KeyError): logger.error( f'Cannot get latest block, bad response ({cls.symbols[0]})') return if not latest_block: logger.debug(f'Latest block not found ({cls.symbols[0]})') return logger.debug(f'Latest block found: {latest_block}') return latest_block
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 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 extract_secret_from_redeem_transaction( cls, contract_address: str) -> Optional[str]: ''' Extracting secret from redeem transaction based on contract address. Args: contract_address (str): address of the contract atomic swap contract Returns: str, None: Secret string or None if contract wasn't redeemed yet. Example: >>> from clove.network import BitcoinTestNet >>> network = BitcoinTestNet() >>> network.extract_secret_from_redeem_transaction('2N7Gxryn4dD1mdyGM3DMxMAwD7k3RBTJ1gP') 90f6b9b9a34acb486654b3e9cdc02cce0b8e40a8845924ffda68453ac2477d20 ''' contract_transactions = clove_req_json( f'{cls.api_url}/addr/{contract_address}')['transactions'] if not contract_transactions: logger.error( f'Cannot get contract transactions ({cls.symbols[0]})') return if len(contract_transactions) < 2: logger.debug( 'There is no redeem transaction on this contract yet.') return redeem_transaction = cls.get_transaction(contract_transactions[0]) if not redeem_transaction: logger.error(f'Cannot get redeem transaction ({cls.symbols[0]})') return return cls.extract_secret( scriptsig=redeem_transaction['vin'][0]['scriptSig']['hex'])
def extract_secret_from_redeem_transaction( cls, contract_address: str) -> Optional[str]: query = """ { allAddressTxes(orderBy: TIME_ASC, condition: { address: "%s" }) { nodes { txId } } } """ % (contract_address) data = clove_req_json(f'{cls.api_url}/graphql', post_data={'query': query}) contract_transactions = data['data']['allAddressTxes']['nodes'] if not contract_transactions: logger.error( f'Cannot get contract transactions ({cls.symbols[0]})') return if len(contract_transactions) < 2: logger.debug( 'There is no redeem transaction on this contract yet.') return redeem_transaction = cls.get_transaction( contract_transactions[1]['txId']) if not redeem_transaction: logger.error(f'Cannot get redeem transaction ({cls.symbols[0]})') return return cls.extract_secret(scriptsig=redeem_transaction['vinsByTxId'] ['nodes'][0]['scriptSig'])
def get_latest_block(cls) -> Optional[int]: '''Returns the number of the latest block.''' query = ''' { allBlocks(orderBy: HEIGHT_DESC, first: 1) { nodes { height } } } ''' try: json_response = clove_req_json(f'{cls.api_url}/graphql', post_data={'query': query}) latest_block = json_response['data']['allBlocks']['nodes'][0][ 'height'] except (TypeError, KeyError): logger.error( f'Cannot get latest block, bad response ({cls.symbols[0]})') return if not latest_block: logger.debug(f'Latest block not found ({cls.symbols[0]})') return logger.debug(f'Latest block found: {latest_block}') return latest_block
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 extract_secret_from_redeem_transaction( cls, contract_address: str) -> Optional[str]: ''' Extracting secret from redeem transaction based on contract address. Args: contract_address (str): address of the contract atomic swap contract Returns: str, None: Secret string or None if contract wasn't redeemed yet. Example: >>> from clove.network import BitcoinTestNet >>> network = BitcoinTestNet() >>> network.extract_secret_from_redeem_transaction('2N7Gxryn4dD1mdyGM3DMxMAwD7k3RBTJ1gP') 90f6b9b9a34acb486654b3e9cdc02cce0b8e40a8845924ffda68453ac2477d20 ''' data = clove_req_json( f'{cls.blockcypher_url()}/addrs/{contract_address}/full') if not data: logger.error('Unexpected response from blockcypher') raise ValueError('Unexpected response from blockcypher') transactions = data['txs'] if len(transactions) == 1: logger.debug('Contract was not redeemed yet.') return return cls.extract_secret( scriptsig=transactions[0]['inputs'][0]['script'])
def create_connection(self, node: str, timeout=2) -> Optional[socket.socket]: ''' Establish connection to a given node. Args: node (str): node domain or IP address timeout (int): number of seconds to wait before raising timeout Returns: socket.socket: socket connection Example: >>> network.create_connection('104.248.185.143') <socket.socket fd=11, family=AddressFamily.AF_INET, type=2049, proto=6, laddr=('10.93.5.21', 36086), raddr=('104.248.185.143', 18333)> # noqa: E501 ''' try: self.connection = socket.create_connection(address=(node, self.port), timeout=timeout) except (socket.timeout, ConnectionRefusedError, OSError): logger.debug('[%s] Could not establish connection to this node', node) return logger.debug('[%s] Connection established, sending version packet', node) if self.send_version(): return self.connection
def get_last_transactions(network: str) -> Optional[list]: resp = clove_req( f'https://chainz.cryptoid.info/{network}/api.dws?q=lasttxs') if not resp or resp.status != 200: logger.debug('Could not get last transactions for %s network', network) return return [t['hash'] for t in json.loads(resp.read().decode())]
def get_nodes(seed) -> list: logger.debug('Getting nodes from seed node %s', seed) try: hostname, alias, nodes = socket.gethostbyname_ex(seed) except (socket.herror, socket.gaierror): return [] logger.debug('Got %s nodes', len(nodes)) return nodes
def connect(self) -> Optional[str]: ''' Connects to some node from the network. Returns: str, None: node IP address or domain, None if doesn't connect to any node Example: >>> network.connect() '198.251.83.19' >>> network.connection <socket.socket fd=12, family=AddressFamily.AF_INET, type=2049, proto=6, laddr=('10.93.5.21', 54300), raddr=('198.251.83.19', 18333)> # noqa: E501 ''' if self.connection and self.send_ping(): # already connected return self.get_current_node() if self.nodes: # fake seed node to enter the seed nodes loop self.seeds = (None, ) random_seeds = list(self.seeds) shuffle(random_seeds) for seed in random_seeds: if seed is None: # get hardcoded nodes nodes = self.nodes else: # get nodes from seed node nodes = self.get_nodes(seed) nodes = self.filter_blacklisted_nodes(nodes) for node in nodes: if not self.create_connection(node): self.terminate(node) continue messages = self.capture_messages([msg_version, msg_verack]) if not messages: logger.debug( '[%s] Failed to get version or version acknowledge message from node', node) self.terminate(node) continue logger.debug( '[%s] Got version, sending version acknowledge message', node) if not self.send_verack(): self.terminate(node) continue return node
def send_message(self, msg: object, timeout: int = 2) -> bool: try: self.connection.settimeout(timeout) self.connection.send(msg.to_bytes()) except (socket.timeout, ConnectionRefusedError, OSError) as e: logger.debug('Failed to send %s message', msg.command.decode()) logger.debug(e) return False return True
def _get_block_hash(cls, block_number: int) -> str: '''Getting block hash by its number''' try: block_hash = clove_req_json(f'{cls.api_url}/block-index/{block_number}')['blockHash'] except (TypeError, KeyError): logger.error(f'Cannot get block hash for block {block_number} ({cls.symbols[0]})') return logger.debug(f'Found hash for block {block_number}: {block_hash}') return block_hash
def get_balance(cls, wallet_address: str) -> float: api_key = os.environ.get('CRYPTOID_API_KEY') if api_key is None: raise ValueError('API key for cryptoid is required to get balance.') data = clove_req_json(f'{cls.cryptoid_url()}/api.dws?q=getbalance&a={wallet_address}&key={api_key}') if data is None: logger.debug('Could not get details for address %s in %s network', wallet_address, cls.symbols[0]) return return data
def get_current_fee(network: str) -> Optional[float]: """Getting current network fee from Clove API""" resp = clove_req_json(f'{CLOVE_API_URL}/fee/{network}') if not resp: logger.debug('Could not get current fee for %s network', network) return return resp['fee']
def extract_secret_from_redeem_transaction( cls, contract_address: str) -> Optional[str]: contract_transactions = clove_req_json( f'https://mona.chainseeker.info/api/v1/txids/{contract_address}') if len(contract_transactions) < 2: logger.debug( 'There is no redeem transaction on this contract yet.') return redeem_transaction = cls.get_transaction(contract_transactions[1]) return cls.extract_secret(redeem_transaction['hex'])
def get_balance_blockcypher(network: str, address: str, testnet: bool) -> Optional[float]: subnet = 'test3' if testnet else 'main' url = f'https://api.blockcypher.com/v1/{network.lower()}/{subnet}/addrs/{address}/full?limit=2000' data = clove_req_json(url) if data is None: logger.debug('Could not get details for address %s in %s network', address, network) return return from_base_units(data['balance'])
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 extract_secret_from_redeem_transaction(self, tx_address: str) -> str: tx_dict = self.get_transaction(tx_address) method_id = self.extract_method_id(tx_dict['input']) if method_id != self.redeem: logger.debug('Not a redeem transaction.') raise ValueError('Not a redeem transaction.') method_name = self.get_method_name(method_id) input_types = get_abi_input_types(find_matching_fn_abi(self.abi, fn_identifier=method_name)) input_values = decode_abi(input_types, Web3.toBytes(hexstr=tx_dict['input'][10:])) return input_values[0].hex()
def _get_transactions_from_block(cls, block_number: int): '''Getting transactions from block by given block number''' block_hash = cls._get_block_hash(block_number) if not block_hash: return transactions_page = clove_req_json(f'{cls.api_url}/txs/?block={block_hash}') if not transactions_page: return transactions = transactions_page['txs'] logger.debug(f'Found {len(transactions)} in block {block_number}') return transactions
def get_transaction_size(network: str, tx_hash: str) -> Optional[int]: """WARNING: this method is using undocumented endpoint used by chainz.cryptoid.info site.""" resp = clove_req( f'https://chainz.cryptoid.info/explorer/tx.raw.dws?coin={network}&id={tx_hash}' ) if not resp or resp.status != 200: logger.debug('Could not get transaction %s size for %s network', tx_hash, network) return tx_details = json.loads(resp.read().decode()) return tx_details['size']
def get_latest_block(cls) -> Optional[int]: '''Returns the number of the latest block.''' try: latest_block = clove_req_json(f'{cls.api_url}/status?q=getInfo')['info']['blocks'] except (TypeError, KeyError): logger.error(f'Cannot get latest block, bad response ({cls.symbols[0]})') return if not latest_block: logger.debug(f'Latest block not found ({cls.symbols[0]})') return logger.debug(f'Latest block found: {latest_block}') return latest_block
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)