예제 #1
0
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
예제 #2
0
    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
예제 #3
0
    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()
예제 #4
0
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'))
예제 #5
0
파일: utils.py 프로젝트: sveitser/rotki
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'))
예제 #6
0
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)
예제 #7
0
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'))
예제 #8
0
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'))
예제 #9
0
파일: utils.py 프로젝트: AndrewBezold/rotki
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'))
예제 #10
0
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
예제 #11
0
    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}
예제 #12
0
파일: utils.py 프로젝트: sveitser/rotki
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)
예제 #13
0
    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}
예제 #14
0
    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()