def _transform_btc_address( ethereum: EthereumManager, given_address: str, ) -> BTCAddress: """Returns a SegWit/P2PKH/P2SH address (if existing) given an ENS domain. NB: ENS domains for BTC store the scriptpubkey. Check EIP-2304. """ if not given_address.endswith('.eth'): return BTCAddress(given_address) resolved_address = ethereum.ens_lookup( given_address, blockchain=SupportedBlockchain.BITCOIN, ) if resolved_address is None: raise ValidationError( f'Given ENS address {given_address} could not be resolved for Bitcoin', field_name='address', ) from None try: address = scriptpubkey_to_btc_address(bytes.fromhex(resolved_address)) except EncodingError as e: raise ValidationError( f'Given ENS address {given_address} does not contain a valid Bitcoin ' f"scriptpubkey: {resolved_address}. Bitcoin address can't be obtained.", field_name='address', ) from e log.debug(f'Resolved BTC ENS {given_address} to {address}') return address
def query_btc_account_balance(account: BTCAddress) -> FVal: """Queries blockchain.info for the balance of account May raise: - RemotError if there is a problem querying blockchain.info or blockcypher """ try: if account.lower()[0:3] == 'bc1': url = f'https://api.blockcypher.com/v1/btc/main/addrs/{account.lower()}/balance' response_data = request_get(url=url) if 'balance' not in response_data: raise RemoteError(f'Unexpected blockcypher balance response: {response_data}') btc_resp = response_data['balance'] else: btc_resp = request_get_direct( url='https://blockchain.info/q/addressbalance/%s' % account, handle_429=True, # If we get a 429 then their docs suggest 10 seconds # https://blockchain.info/q backoff_in_seconds=10, ) except (requests.exceptions.ConnectionError, UnableToDecryptRemoteData) as e: raise RemoteError(f'bitcoin external API request failed due to {str(e)}') return satoshis_to_btc(FVal(btc_resp)) # result is in satoshis
def modify_blockchain_accounts( self, blockchain: SupportedBlockchain, accounts: ListOfBlockchainAddresses, append_or_remove: str, add_or_sub: Callable[[FVal, FVal], FVal], ) -> BlockchainBalancesUpdate: """Add or remove a list of blockchain account May raise: - InputError if accounts to remove do not exist. - EthSyncError if there is a problem querying the ethereum chain - RemoteError if there is a problem querying an external service such as etherscan or blockchain.info """ if blockchain == SupportedBlockchain.BITCOIN: for account in accounts: self.modify_btc_account( BTCAddress(account), append_or_remove, add_or_sub, ) elif blockchain == SupportedBlockchain.ETHEREUM: for account in accounts: address = deserialize_ethereum_address(account) try: self.modify_eth_account( account=address, append_or_remove=append_or_remove, add_or_sub=add_or_sub, ) except BadFunctionCallOutput as e: log.error( 'Assuming unsynced chain. Got web3 BadFunctionCallOutput ' 'exception: {}'.format(str(e)), ) raise EthSyncError( 'Tried to use the ethereum chain of a local client to edit ' 'an eth account but the chain is not synced.', ) for _, module in self.eth_modules.items(): if append_or_remove == 'append': module.on_account_addition(address) else: # remove module.on_account_removal(address) else: # That should not happen. Should be checked by marshmallow raise AssertionError( 'Unsupported blockchain {} provided at remove_blockchain_account'.format( blockchain), ) return self.get_balances_update()
def scriptpubkey_to_p2sh_address(data: bytes) -> BTCAddress: """Return a P2SH address given a scriptpubkey P2SH: OP_HASH160 <scriptHash> OP_EQUAL """ if data[0:1] != OpCodes.op_hash160 or data[-1:] != OpCodes.op_equal: raise EncodingError(f'Invalid P2SH scriptpubkey: {data.hex()}') prefixed_hash = bytes.fromhex('05') + data[2:22] # 20 byte pubkey hash checksum = hashlib.sha256(hashlib.sha256(prefixed_hash).digest()).digest() address = base58check.b58encode(prefixed_hash + checksum[:4]) return BTCAddress(address.decode('ascii'))
def pubkey_to_base58_address(data: bytes) -> BTCAddress: """ Bitcoin pubkey to base58 address Source: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses#How_to_create_Bitcoin_Address https://hackernoon.com/how-to-generate-bitcoin-addresses-technical-address-generation-explanation-rus3z9e May raise: - ValueError, TypeError due to b58encode """ prefixed_hash, checksum = _calculate_hash160_and_checksum(b'\x00', data) return BTCAddress(base58check.b58encode(prefixed_hash + checksum[:4]).decode('ascii'))
def scriptpubkey_to_bech32_address(data: bytes) -> BTCAddress: """Return a native SegWit (bech32) address given a scriptpubkey""" version = data[0] if OpCodes.op_1 <= data[0:1] <= OpCodes.op_16: version -= 0x50 elif data[0:1] != OpCodes.op_0: raise EncodingError(f'Invalid bech32 scriptpubkey: {data.hex()}') address = bech32.encode('bc', version, data[2:]) if not address: # should not happen raise EncodingError( 'Could not derive bech32 address from given scriptpubkey') return BTCAddress(address)
def scriptpubkey_to_p2pkh_address(data: bytes) -> BTCAddress: """Return a P2PKH address given a scriptpubkey P2PKH: OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG """ if (data[0:1] != OpCodes.op_dup or data[1:2] != OpCodes.op_hash160 or data[-2:-1] != OpCodes.op_equalverify or data[-1:] != OpCodes.op_checksig): raise EncodingError(f'Invalid P2PKH scriptpubkey: {data.hex()}') prefixed_hash = bytes.fromhex('00') + data[3:23] # 20 byte pubkey hash checksum = hashlib.sha256(hashlib.sha256(prefixed_hash).digest()).digest() address = base58check.b58encode(prefixed_hash + checksum[:4]) return BTCAddress(address.decode('ascii'))
def pubkey_to_p2sh_p2wpkh_address(data: bytes) -> BTCAddress: """Bitcoin pubkey to PS2H-P2WPKH From here: https://bitcoin.stackexchange.com/questions/75910/how-to-generate-a-native-segwit-address-and-p2sh-segwit-address-from-a-standard """ witprog = hash160(data) script = bytes.fromhex('0014') + witprog prefix = b'\x05' # this is mainnet prefix -- we don't care about testnet # prefixed_hash, checksum = _calculate_hash160_and_checksum(prefix, prefix + script) prefixed_hash, checksum = _calculate_hash160_and_checksum(prefix, script) # address = base58check.b58encode(prefix + prefixed_hash + checksum[:4]) address = base58check.b58encode(prefixed_hash + checksum[:4]) return BTCAddress(address.decode('ascii'))
def pubkey_to_base58_address(data: bytes) -> BTCAddress: """ Bitcoin pubkey to base58 address Source: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses#How_to_create_Bitcoin_Address https://hackernoon.com/how-to-generate-bitcoin-addresses-technical-address-generation-explanation-rus3z9e May raise: - ValueError, TypeError due to b58encode """ s4 = b'\x00' + hash160(data) s5 = hashlib.sha256(s4).digest() s6 = hashlib.sha256(s5).digest() return BTCAddress(base58check.b58encode(s4 + s6[:4]).decode('ascii'))
def _prepare_blockcypher_accounts( accounts: List[BTCAddress]) -> List[BTCAddress]: """bech32 accounts have to be given lowercase to the blockcypher query. No idea why. """ new_accounts: List[BTCAddress] = [] for x in accounts: lowered = x.lower() if lowered[0:3] == 'bc1': new_accounts.append(BTCAddress(lowered)) else: new_accounts.append(x) return new_accounts
def modify_blockchain_account( self, blockchain: SupportedBlockchain, account: BlockchainAddress, append_or_remove: str, add_or_sub: Callable[[FVal, FVal], FVal], ) -> BlockchainBalancesUpdate: """Add or remove a blockchain account May raise: - InputError if accounts to remove do not exist or if the ethereum/BTC addresses are not valid. - EthSyncError if there is a problem querying the ethereum chain - RemoteError if there is a problem querying an external service such as etherscan or blockchain.info """ if blockchain == SupportedBlockchain.BITCOIN: if append_or_remove == 'remove' and account not in self.accounts.btc: raise InputError('Tried to remove a non existing BTC account') # above we check that account is a BTC account self.modify_btc_account( BTCAddress(account), append_or_remove, add_or_sub, ) elif blockchain == SupportedBlockchain.ETHEREUM: try: # above we check that account is an ETH account self.modify_eth_account(EthAddress(account), append_or_remove, add_or_sub) except BadFunctionCallOutput as e: log.error( 'Assuming unsynced chain. Got web3 BadFunctionCallOutput ' 'exception: {}'.format(str(e)), ) raise EthSyncError( 'Tried to use the ethereum chain of a local client to edit ' 'an eth account but the chain is not synced.', ) else: # That should not happen. Should be checked by marshmallow raise AssertionError( 'Unsupported blockchain {} provided at remove_blockchain_account' .format(blockchain), ) return {'per_account': self.balances, 'totals': self.totals}
def pubkey_to_bech32_address(data: bytes, witver: int) -> BTCAddress: """ Bitcoin pubkey to bech32 address Source: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#witness-program https://github.com/mcdallas/cryptotools/blob/master/btctools/address.py May raise: - EncodingError if address could not be derived from public key """ witprog = hash160(data) result = bech32.encode('bc', witver, witprog) if not result: raise EncodingError('Could not derive bech32 address from given public key') return BTCAddress(result)
def modify_blockchain_account( self, blockchain: SupportedBlockchain, account: BlockchainAddress, append_or_remove: str, add_or_sub: Callable[[FVal, FVal], FVal], ) -> BlockchainBalancesUpdate: if blockchain == SupportedBlockchain.BITCOIN: if append_or_remove == 'remove' and account not in self.accounts.btc: raise InputError('Tried to remove a non existing BTC account') # above we check that account is a BTC account self.modify_btc_account( BTCAddress(account), append_or_remove, add_or_sub, ) elif blockchain == SupportedBlockchain.ETHEREUM: if append_or_remove == 'remove' and account not in self.accounts.eth: raise InputError('Tried to remove a non existing ETH account') try: # above we check that account is an ETH account self.modify_eth_account(EthAddress(account), append_or_remove, add_or_sub) except BadFunctionCallOutput as e: log.error( 'Assuming unsynced chain. Got web3 BadFunctionCallOutput ' 'exception: {}'.format(str(e)), ) raise EthSyncError( 'Tried to use the ethereum chain of a local client to edit ' 'an eth account but the chain is not synced.', ) else: raise InputError( 'Unsupported blockchain {} provided at remove_blockchain_account' .format(blockchain), ) return {'per_account': self.balances, 'totals': self.totals}
def modify_blockchain_accounts( self, blockchain: SupportedBlockchain, accounts: ListOfBlockchainAddresses, append_or_remove: str, add_or_sub: AddOrSub, already_queried_balances: Optional[List[FVal]] = None, ) -> BlockchainBalancesUpdate: """Add or remove a list of blockchain account May raise: - InputError if accounts to remove do not exist. - EthSyncError if there is a problem querying the ethereum chain - RemoteError if there is a problem querying an external service such as etherscan or blockchain.info """ if blockchain == SupportedBlockchain.BITCOIN: for idx, account in enumerate(accounts): a_balance = already_queried_balances[ idx] if already_queried_balances else None self.modify_btc_account( BTCAddress(account), append_or_remove, add_or_sub, already_queried_balance=a_balance, ) elif blockchain == SupportedBlockchain.ETHEREUM: for account in accounts: address = deserialize_ethereum_address(account) try: self.modify_eth_account( account=address, append_or_remove=append_or_remove, ) except BadFunctionCallOutput as e: log.error( 'Assuming unsynced chain. Got web3 BadFunctionCallOutput ' 'exception: {}'.format(str(e)), ) raise EthSyncError( 'Tried to use the ethereum chain of a local client to edit ' 'an eth account but the chain is not synced.', ) # Also modify and take into account defi balances if append_or_remove == 'append': balances = self.zerion.all_balances_for_account(address) if len(balances) != 0: self.defi_balances[address] = balances self._add_account_defi_balances_to_token_and_totals( account=address, balances=balances, ) else: # remove self.defi_balances.pop(address, None) # For each module run the corresponding callback for the address for _, module in self.iterate_modules(): if append_or_remove == 'append': module.on_account_addition(address) else: # remove module.on_account_removal(address) else: # That should not happen. Should be checked by marshmallow raise AssertionError( 'Unsupported blockchain {} provided at remove_blockchain_account' .format(blockchain), ) return self.get_balances_update()