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 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 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 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 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_balance(cls, wallet_address: str) -> Optional[float]: data = clove_req_json( f'{cls.blockcypher_url()}/addrs/{wallet_address}/balance') if data is None: logger.error('Could not get details for address %s in %s network', wallet_address, cls.symbols[0]) return return from_base_units(data['balance'] or data['unconfirmed_balance'])
def capture_messages(self, expected_message_types: list, timeout: int = 20, buf_size: int = 1024, ignore_empty: bool = False) -> list: deadline = time() + timeout found = [] partial_message = None while expected_message_types and time() < deadline: try: received_data = self.connection.recv(buf_size) if partial_message: received_data = partial_message + received_data except socket.timeout: continue if not received_data: sleep(0.1) continue for raw_message in self.split_message(received_data): try: message = MsgSerializable.from_bytes(raw_message) except (SerializationError, SerializationTruncationError, ValueError): partial_message = raw_message continue partial_message = None if not message: # unknown message type, skipping continue msg_type = type(message) if msg_type is msg_ping: logger.debug('Got ping, sending pong.') self.send_pong(message) elif msg_type is msg_version: logger.debug('Saving version') self.protocol_version = message if msg_type in expected_message_types: found.append(message) expected_message_types.remove(msg_type) logger.debug('Found %s, %s more to catch', msg_type.command.upper(), len(expected_message_types)) if not expected_message_types: return found if not ignore_empty: logger.error('Not all messages could be captured')
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_fee(cls) -> Optional[float]: '''Returns actual fee per kb.''' response = clove_req_json(cls.fee_endpoint) fee = response.get('high_fee_per_kb') if not fee: logger.error( 'Cannot find the right key (high_fee_per_kb) while getting fee in blockcypher.' ) return return from_base_units(fee)
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 extract_secret_from_redeem_transaction(cls, contract_address: str) -> Optional[str]: contract_transactions = clove_req_json(f'{cls.api_url}/txids/{contract_address}') 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]) if not redeem_transaction: logger.error(f'Cannot get redeem transaction ({cls.symbols[0]})') return return cls.extract_secret(redeem_transaction['hex'])
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 clove_req_json(url: str, post_data={}): """ Make a request with Clove user-agent header and return json response Args: url (str): url to get data from Returns: dict: response data Raises: ExternalApiRequestLimitExceeded: if response status code is 429 Example: >>> from clove.utils.external_source import clove_req_json >>> clove_req_json('https://testnet.blockexplorer.com/api/status?q=getInfo') >>> {'info': { >>> 'blocks': 1414831, >>> 'connections': 23, >>> 'difficulty': 1, >>> 'errors': 'Warning: unknown new rules activated (versionbit 28)', >>> 'network': 'testnet', >>> 'protocolversion': 70012, >>> 'proxy': '', >>> 'relayfee': 1e-05, >>> 'testnet': True, >>> 'timeoffset': 0, >>> 'version': 120100}} """ logger.debug(' Requesting: %s', url) request_start = time.time() if post_data: resp = requests.post(url, data=post_data) else: resp = requests.get(url, headers={'User-Agent': 'Clove'}) response_time = time.time() - request_start logger.debug('Got response: %s [%.2fs]', url, response_time) if resp.status_code == 429: logger.error(f'Requests limit exceeded when requesting url: {url}') raise ExternalApiRequestLimitExceeded(f'url: {url}') if resp.status_code != 200: logger.error(f'Unexpected status code when requesting url: {url}') logger.debug(resp.content) return return resp.json()
def extract_secret_from_redeem_transaction( cls, contract_address: str) -> Optional[str]: 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 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 atomic_swap( self, sender_address: str, recipient_address: str, value: float, solvable_utxo: list = None, secret_hash: str = None, ) -> BitcoinAtomicSwapTransaction: if not solvable_utxo: solvable_utxo = self.get_utxo(sender_address, value) if not solvable_utxo: logger.error(f'Cannot get UTXO for address {sender_address}') return transaction = BitcoinAtomicSwapTransaction(self, sender_address, recipient_address, value, solvable_utxo, secret_hash) transaction.create_unsigned_transaction() return transaction
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 ''' response = clove_req_json(cls.blockcypher_url()) fee = response.get('high_fee_per_kb') if not fee: logger.error( 'Cannot find the right key (high_fee_per_kb) while getting fee in blockcypher.' ) return return from_base_units(fee)
def publish(cls, raw_transaction: str) -> str: ''' Publish signed transaction via block explorer API. Args: raw_transaction (str): signed transaction Returns: str: transaction address if transaction was published Raises: ValueError: if something went wrong ''' url = f'{cls.api_url}/tx/send' logger.debug(' Requesting: %s', url) request_start = time.time() response = requests.post(url, data={'rawtx': raw_transaction}) response_time = time.time() - request_start logger.debug('Got response: %s [%.2fs]', url, response_time) if response.status_code == 200: try: return response.json()['txid'] except (JSONDecodeError, TypeError, KeyError): logger.error( f'Unexpected response from API when publishing transaction ({cls.symbols[0]})' ) raise ValueError( 'Unexpected response format from API. Please try again.') if response.status_code == 400: logger.debug( f'Error while publishing transaction via API ({cls.symbols[0]}): {response.text}' ) raise ValueError( f'Unexepected error: ({response.text}). Please try again or check your transaction.' ) logger.error( f'Unexepected error while publishing transaction via API ({cls.symbols[0]}). ' 'Status code: {response.status.code}') raise ValueError('Unexpected response from API. Please try again.')
def atomic_swap( self, sender_address: str, recipient_address: str, value: float, solvable_utxo: list = None, secret_hash: str = None, ) -> Optional[BitcoinAtomicSwapTransaction]: ''' Creates atomic swap unsigned transaction object. Args: sender_address (str): wallet address of the sender recipient_address (str): wallet address of the recipient value (float): amount to swap solvable_utxo (list): optional list of UTXO objects. If None then it will try to find UTXO automatically by using the `get_utxo` method. secret_hash (str): optional secret hash to be used in transaction. If None then the new hash will be generated. Returns: BitcoinAtomicSwapTransaction, None: unsigned Atomic Swap transaction object or None if something went wrong Example: >>> from clove.network import BitcoinTestNet >>> network = BitcoinTestNet() >>> network.atomic_swap('msJ2ucZ2NDhpVzsiNE5mGUFzqFDggjBVTM', 'mmJtKA92Mxqfi3XdyGReza69GjhkwAcBN1', 2.4) <clove.network.bitcoin.transaction.BitcoinAtomicSwapTransaction at 0x7f989439d630> ''' if not solvable_utxo: solvable_utxo = self.get_utxo(sender_address, value) if not solvable_utxo: logger.error(f'Cannot get UTXO for address {sender_address}') return transaction = BitcoinAtomicSwapTransaction(self, sender_address, recipient_address, value, solvable_utxo, secret_hash) transaction.create_unsigned_transaction() return transaction
def get_balance(cls, wallet_address: str) -> Optional[float]: ''' Returns wallet balance without unconfirmed transactions. Args: wallet_address (str): wallet address Returns: float, None: account balance converted from base units or None if something went wrong Example: >>> from clove.network import BitcoinTestNet >>> network = BitcoinTestNet() >>> network.get_balance('msJ2ucZ2NDhpVzsiNE5mGUFzqFDggjBVTM') 4.22188744 ''' data = clove_req_json( f'{cls.blockcypher_url()}/addrs/{wallet_address}/balance') if data is None: logger.error('Could not get details for address %s in %s network', wallet_address, cls.symbols[0]) return return from_base_units(data['balance'] or data['unconfirmed_balance'])
def capture_messages(self, expected_message_types: list, timeout: int = 20, buf_size: int = 1024, ignore_empty: bool = False) -> list: ''' Method for receiving messages from network nodes. Args: expected_message_types (list): list of message types to search for timeout (int): timeout for waiting for messages buf_size (int): buffer size that is going to be used for receiving messages ignore_empty (bool): flag for ignoring errors if the message that we're looking for wasn't found. Eg. this flag can be set to True when looking for reject messages because the absence of the reject message is not an error. Returns: list, None: list of received messages or None if none or not all expected message types were found Example: >>> from bitcoin.messages import msg_verack, msg_version >>> network.capture_messages([msg_version, msg_verack]) ''' deadline = time() + timeout found = [] partial_message = None while expected_message_types and time() < deadline: try: received_data = self.connection.recv(buf_size) if partial_message: received_data = partial_message + received_data except socket.timeout: continue if not received_data: sleep(0.1) continue for raw_message in self.split_message(received_data): try: message = MsgSerializable.from_bytes(raw_message) except (SerializationError, SerializationTruncationError, ValueError): partial_message = raw_message continue partial_message = None if not message: # unknown message type, skipping continue msg_type = type(message) if msg_type is msg_ping: logger.debug('Got ping, sending pong.') self.send_pong(message) elif msg_type is msg_version: logger.debug('Saving version') self.protocol_version = message if msg_type in expected_message_types: found.append(message) expected_message_types.remove(msg_type) logger.debug('Found %s, %s more to catch', msg_type.command.upper(), len(expected_message_types)) if not expected_message_types: return found if not ignore_empty: logger.error('Not all messages could be captured')
def create_github_links(line): search_base = 'https://api.github.com/search/code?q=op_checklocktimeverify+in:file+repo:' repo_info_base = 'https://api.github.com/repos/' repo_name = line.strip().split('github.com/')[1] repo_name = repo_name.split('/blob/')[0] repo_zip_file = f'https://github.com/{repo_name}/archive/master.zip' return search_base + repo_name, repo_info_base + repo_name, repo_zip_file if __name__ == '__main__': if get_github_api_token() is None: logger.error( 'GITHUB_API_TOKEN environment variable is required to run this script.' ) exit(1) base_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) network_dir = os.path.join(base_dir, 'clove/network/bitcoin_based/') files = sorted(glob.glob(network_dir + '*')) found = [] fork = [] failed = [] os.makedirs('repos_zip', exist_ok=True) for file_path in files: if not os.path.isfile(file_path):