示例#1
0
def test_price_underlying_tokens(inquirer, globaldb):
    aave_weight, link_weight, crv_weight = FVal('0.6'), FVal('0.2'), FVal(
        '0.2')
    address = make_ethereum_address()
    token = EthereumToken.initialize(
        address=address,
        decimals=18,
        name='Test',
        symbol='YAB',
        underlying_tokens=[
            UnderlyingToken(address=A_AAVE.ethereum_address,
                            weight=aave_weight),
            UnderlyingToken(address=A_LINK.ethereum_address,
                            weight=link_weight),
            UnderlyingToken(address=A_CRV.ethereum_address, weight=crv_weight),
        ],
    )
    globaldb.add_asset(
        asset_id=ethaddress_to_identifier(address),
        asset_type=AssetType.ETHEREUM_TOKEN,
        data=token,
    )

    price = inquirer.find_price(EthereumToken(address), A_USD)
    assert price == FVal(67)
示例#2
0
    def transform_data(  # pylint: disable=no-self-use
            self,
            data: Dict[str, Any],
            **_kwargs: Any,
    ) -> Dict[str, Any]:
        """Returns the a dictionary with:
        - The identifier
        - extra_information used by the globaldb handler
        - name
        - symbol
        - asset_type as instance of AssetType
        """
        given_underlying_tokens = data.pop('underlying_tokens', None)
        underlying_tokens = None
        if given_underlying_tokens is not None:
            underlying_tokens = []
            for entry in given_underlying_tokens:
                underlying_tokens.append(UnderlyingToken(
                    address=entry['address'],
                    weight=entry['weight'],
                ))

        asset_type = data['asset_type']
        extra_information: Union[Dict[str, Any], EthereumToken]
        swapped_for, swapped_for_ident = data.pop('swapped_for'), None
        if swapped_for is not None:
            swapped_for_ident = swapped_for.identifier

        if asset_type == AssetType.ETHEREUM_TOKEN:
            extra_information = EthereumToken.initialize(
                address=data.pop('ethereum_address'),
                name=data.get('name'),
                symbol=data.get('symbol'),
                decimals=data.pop('decimals'),
                started=data.pop('started'),
                swapped_for=swapped_for_ident,
                coingecko=data.pop('coingecko'),
                cryptocompare=data.pop('cryptocompare'),
                underlying_tokens=underlying_tokens,
            )
        else:
            forked, forked_ident = data.pop('forked'), None
            if forked is not None:
                forked_ident = forked.identifier

            extra_information = {
                'name': data.get('name'),
                'symbol': data.get('symbol'),
                'started': data.pop('started'),
                'forked': forked_ident,
                'swapper_for': swapped_for_ident,
                'coingecko': data.pop('coingecko'),
                'cryptocompare': data.pop('cryptocompare'),
            }

        data['underlying_tokens'] = underlying_tokens
        data['asset_type'] = asset_type
        data['extra_information'] = extra_information
        return data
示例#3
0
文件: handler.py 项目: jsloane/rotki
    def fetch_underlying_tokens(
        address: ChecksumEthAddress, ) -> Optional[List[UnderlyingToken]]:
        """Fetch underlying tokens for a token address if they exist"""
        cursor = GlobalDBHandler()._conn.cursor()
        query = cursor.execute(
            'SELECT address, weight from underlying_tokens_list WHERE parent_token_entry=?;',
            (address, ),
        )
        results = query.fetchall()
        underlying_tokens = None
        if len(results) != 0:
            underlying_tokens = [
                UnderlyingToken.deserialize_from_db(x) for x in results
            ]

        return underlying_tokens
示例#4
0
BALANCER_TEST_ADDR1 = string_to_ethereum_address(
    '0x49a2DcC237a65Cc1F412ed47E0594602f6141936')
BALANCER_TEST_ADDR2 = string_to_ethereum_address(
    '0x029f388aC4D5C8BfF490550ce0853221030E822b')
BALANCER_TEST_ADDR3 = string_to_ethereum_address(
    '0x7716a99194d758c8537F056825b75Dd0C8FDD89f')
BALANCER_TEST_ADDR4 = string_to_ethereum_address(
    '0x231DC6af3C66741f6Cf618884B953DF0e83C1A2A')
BALANCER_TEST_ADDR3_POOL1 = EthereumToken.initialize(
    address=string_to_ethereum_address(
        '0x59A19D8c652FA0284f44113D0ff9aBa70bd46fB4'),
    symbol='BPT',
    protocol='balancer',
    underlying_tokens=[
        UnderlyingToken(address=string_to_ethereum_address(
            '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'),
                        weight=FVal(0.2)),  # noqa: E501  # WETH
        UnderlyingToken(address=string_to_ethereum_address(
            '0xba100000625a3754423978a60c9317c58a424e3D'),
                        weight=FVal(0.8)),  # noqa: E501  # BAL
    ],
)
BALANCER_TEST_ADDR3_POOL2 = EthereumToken.initialize(
    address=string_to_ethereum_address(
        '0x574FdB861a0247401B317a3E68a83aDEAF758cf6'),
    symbol='BPT',
    protocol='balancer',
    underlying_tokens=[
        UnderlyingToken(address=string_to_ethereum_address(
            '0x0D8775F648430679A709E98d2b0Cb6250d2887EF'),
                        weight=FVal(0.1)),  # noqa: E501  # BAT
示例#5
0
def test_global_db_reset(globaldb):
    """
    Check that the user can recreate assets information from the packaged
    database with rotki (soft reset). The test adds a new asset, restores
    the database and checks that the added tokens are still in the database.
    In addition a token is edited and we check that was correctly restored.
    """
    # Add a custom eth token
    address_to_delete = make_ethereum_address()
    token_to_delete = EthereumToken.initialize(
        address=address_to_delete,
        decimals=18,
        name='willdell',
        symbol='DELME',
    )
    globaldb.add_asset(
        asset_id='DELMEID1',
        asset_type=AssetType.ETHEREUM_TOKEN,
        data=token_to_delete,
    )
    # Add a token with underlying token
    with_underlying_address = make_ethereum_address()
    with_underlying = EthereumToken.initialize(
        address=with_underlying_address,
        decimals=18,
        name="Not a scam",
        symbol="NSCM",
        started=0,
        underlying_tokens=[
            UnderlyingToken(
                address=address_to_delete,
                weight=1,
            )
        ],
    )
    globaldb.add_asset(
        asset_id='xDELMEID1',
        asset_type=AssetType.ETHEREUM_TOKEN,
        data=with_underlying,
    )
    # Add asset that is not a token
    globaldb.add_asset(
        asset_id='1',
        asset_type=AssetType.OWN_CHAIN,
        data={
            'name': 'Lolcoin',
            'symbol': 'LOLZ',
            'started': 0,
        },
    )
    # Edit one token
    one_inch_update = EthereumToken.initialize(
        address='0x111111111117dC0aa78b770fA6A738034120C302',
        name='1inch boi',
    )
    GlobalDBHandler().edit_ethereum_token(one_inch_update)

    status, _ = GlobalDBHandler().soft_reset_assets_list()
    assert status
    cursor = globaldb._conn.cursor()
    query = f'SELECT COUNT(*) FROM ethereum_tokens where address == "{address_to_delete}";'
    r = cursor.execute(query)
    assert r.fetchone() == (
        1, ), 'Custom ethereum tokens should not been deleted'
    query = f'SELECT COUNT(*) FROM assets where details_reference == "{address_to_delete}";'
    r = cursor.execute(query)
    assert r.fetchone() == (1, )
    query = f'SELECT COUNT(*) FROM ethereum_tokens where address == "{with_underlying_address}";'
    r = cursor.execute(query)
    assert r.fetchone() == (
        1, ), 'Ethereum token with underlying token should not be deleted'
    query = f'SELECT COUNT(*) FROM assets where details_reference == "{with_underlying_address}";'
    r = cursor.execute(query)
    assert r.fetchone() == (1, )
    query = f'SELECT COUNT(*) FROM underlying_tokens_list where address == "{address_to_delete}";'
    r = cursor.execute(query)
    assert r.fetchone() == (1, )
    query = 'SELECT COUNT(*) FROM assets where identifier == "1";'
    r = cursor.execute(query)
    assert r.fetchone() == (
        1, ), 'Non ethereum token added should be in the db'
    # Check that the 1inch token was correctly fixed
    assert EthereumToken(
        '0x111111111117dC0aa78b770fA6A738034120C302').name != '1inch boi'

    # Check that the number of assets is the expected
    root_dir = Path(__file__).resolve().parent.parent.parent
    builtin_database = root_dir / 'data' / 'global.db'
    conn = sqlite3.connect(builtin_database)
    cursor_clean_db = conn.cursor()
    tokens_expected = cursor_clean_db.execute('SELECT COUNT(*) FROM assets;')
    tokens_local = cursor.execute('SELECT COUNT(*) FROM assets;')
    assert tokens_expected.fetchone()[0] + 3 == tokens_local.fetchone()[0]
    conn.close()
示例#6
0
def test_global_db_restore(globaldb, database):
    """
    Check that the user can recreate assets information from the packaged
    database with rotki (hard reset). The test adds a new asset, restores
    the database and checks that the added token is not in there and that
    the amount of assets is the expected
    """
    # Add a custom eth token
    address_to_delete = make_ethereum_address()
    token_to_delete = EthereumToken.initialize(
        address=address_to_delete,
        decimals=18,
        name='willdell',
        symbol='DELME',
    )
    globaldb.add_asset(
        asset_id='DELMEID1',
        asset_type=AssetType.ETHEREUM_TOKEN,
        data=token_to_delete,
    )
    # Add a token with underlying token
    with_underlying_address = make_ethereum_address()
    with_underlying = EthereumToken.initialize(
        address=with_underlying_address,
        decimals=18,
        name="Not a scam",
        symbol="NSCM",
        started=0,
        underlying_tokens=[
            UnderlyingToken(
                address=address_to_delete,
                weight=1,
            )
        ],
    )
    globaldb.add_asset(
        asset_id='xDELMEID1',
        asset_type=AssetType.ETHEREUM_TOKEN,
        data=with_underlying,
    )
    # Add asset that is not a token
    globaldb.add_asset(
        asset_id='1',
        asset_type=AssetType.OWN_CHAIN,
        data={
            'name': 'Lolcoin',
            'symbol': 'LOLZ',
            'started': 0,
        },
    )

    # Add asset that is not a token
    globaldb.add_asset(
        asset_id='2',
        asset_type=AssetType.OWN_CHAIN,
        data={
            'name': 'Lolcoin2',
            'symbol': 'LOLZ2',
            'started': 0,
        },
    )

    database.add_asset_identifiers('1')
    database.add_asset_identifiers('2')

    # Try to reset DB it if we have a trade that uses a custom asset
    buy_asset = symbol_to_asset_or_token('LOLZ2')
    buy_amount = deserialize_asset_amount(1)
    sold_asset = symbol_to_asset_or_token('LOLZ')
    sold_amount = deserialize_asset_amount(2)
    rate = Price(buy_amount / sold_amount)
    trade = Trade(
        timestamp=Timestamp(12312312),
        location=Location.BLOCKFI,
        base_asset=buy_asset,
        quote_asset=sold_asset,
        trade_type=TradeType.BUY,
        amount=buy_amount,
        rate=rate,
        fee=None,
        fee_currency=None,
        link='',
        notes="",
    )

    database.add_trades([trade])
    status, _ = GlobalDBHandler().hard_reset_assets_list(database)
    assert status is False
    # Now do it without the trade
    database.delete_trade(trade.identifier)
    status, msg = GlobalDBHandler().hard_reset_assets_list(database, True)
    assert status, msg
    cursor = globaldb._conn.cursor()
    query = f'SELECT COUNT(*) FROM ethereum_tokens where address == "{address_to_delete}";'
    r = cursor.execute(query)
    assert r.fetchone() == (0, ), 'Ethereum token should have been deleted'
    query = f'SELECT COUNT(*) FROM assets where details_reference == "{address_to_delete}";'
    r = cursor.execute(query)
    assert r.fetchone() == (
        0, ), 'Ethereum token should have been deleted from assets'
    query = f'SELECT COUNT(*) FROM ethereum_tokens where address == "{with_underlying_address}";'
    r = cursor.execute(query)
    assert r.fetchone() == (
        0,
    ), 'Token with underlying token should have been deleted from assets'
    query = f'SELECT COUNT(*) FROM assets where details_reference == "{with_underlying_address}";'
    r = cursor.execute(query)
    assert r.fetchone() == (0, )
    query = f'SELECT COUNT(*) FROM underlying_tokens_list where address == "{address_to_delete}";'
    r = cursor.execute(query)
    assert r.fetchone() == (0, )
    query = 'SELECT COUNT(*) FROM assets where identifier == "1";'
    r = cursor.execute(query)
    assert r.fetchone() == (0, ), 'Non ethereum token should be deleted'
    # Check that the user database is correctly updated
    query = 'SELECT identifier from assets'
    r = cursor.execute(query)
    user_db_cursor = database.conn.cursor()
    user_db_cursor.execute(query)
    assert r.fetchall() == user_db_cursor.fetchall()

    # Check that the number of assets is the expected
    root_dir = Path(__file__).resolve().parent.parent.parent
    builtin_database = root_dir / 'data' / 'global.db'
    conn = sqlite3.connect(builtin_database)
    cursor_clean_db = conn.cursor()
    tokens_expected = cursor_clean_db.execute('SELECT COUNT(*) FROM assets;')
    tokens_local = cursor.execute('SELECT COUNT(*) FROM assets;')
    assert tokens_expected.fetchone() == tokens_local.fetchone()
    cursor.execute('SELECT asset_id FROM user_owned_assets')
    msg = 'asset id in trade should not be in the owned table'
    assert "'2'" not in [entry[0] for entry in cursor.fetchall()], msg
    conn.close()
示例#7
0
文件: globaldb.py 项目: jsloane/rotki
custom_address1 = make_ethereum_address()
custom_address2 = make_ethereum_address()
INITIAL_TOKENS = [
    EthereumToken.initialize(
        address=custom_address1,
        decimals=4,
        name='Custom 1',
        symbol='CST1',
        started=Timestamp(0),
        swapped_for=A_MKR,
        coingecko='foo',
        cryptocompare='boo',
        protocol='uniswap',
        underlying_tokens=[
            UnderlyingToken(address=underlying_address1,
                            weight=FVal('0.5055')),
            UnderlyingToken(address=underlying_address2,
                            weight=FVal('0.1545')),
            UnderlyingToken(address=underlying_address3, weight=FVal('0.34')),
        ],
    ),
    EthereumToken.initialize(
        address=custom_address2,
        decimals=18,
        name='Custom 2',
        symbol='CST2',
    ),
]

INITIAL_EXPECTED_TOKENS = [INITIAL_TOKENS[0]] + [
    EthereumToken.initialize(underlying_address1),
示例#8
0
def test_adding_custom_tokens(rotkehlchen_api_server):
    """Test that the endpoint for adding a custom ethereum token works"""
    serialized_token = CUSTOM_TOKEN3.serialize_all_info()
    del serialized_token['identifier']
    response = requests.put(
        api_url_for(
            rotkehlchen_api_server,
            'ethereumassetsresource',
        ),
        json={'token': serialized_token},
    )
    result = assert_proper_response_with_result(response)
    assert result == {'identifier': ETHEREUM_DIRECTIVE + CUSTOM_TOKEN3.ethereum_address}

    response = requests.get(
        api_url_for(
            rotkehlchen_api_server,
            'ethereumassetsresource',
        ),
    )
    result = assert_proper_response_with_result(response)
    expected_tokens = INITIAL_EXPECTED_TOKENS.copy() + [
        CUSTOM_TOKEN3,
        EthereumToken.initialize(address=underlying_address4),
    ]
    expected_result = [x.serialize_all_info() for x in expected_tokens]
    assert_token_entry_exists_in_result(result, expected_result)

    # test that adding an already existing address is handled properly
    serialized_token = INITIAL_TOKENS[1].serialize_all_info()
    del serialized_token['identifier']
    response = requests.put(
        api_url_for(
            rotkehlchen_api_server,
            'ethereumassetsresource',
        ),
        json={'token': serialized_token},
    )
    expected_msg = (
        f'Ethereum token with address {INITIAL_TOKENS[1].ethereum_address} already '
        f'exists in the DB',
    )
    assert_error_response(
        response=response,
        contained_in_msg=expected_msg,
        status_code=HTTPStatus.CONFLICT,
    )

    # also test that the addition of underlying tokens has created proper asset entires for them
    cursor = GlobalDBHandler()._conn.cursor()
    result = cursor.execute(
        'SELECT COUNT(*) from assets WHERE identifier IN (?, ?, ?, ?)',
        [ETHEREUM_DIRECTIVE + x for x in [underlying_address1, underlying_address2, underlying_address3, underlying_address4]],  # noqa: E501
    ).fetchone()[0]
    assert result == 4
    result = cursor.execute(
        'SELECT COUNT(*) from ethereum_tokens WHERE address IN (?, ?, ?, ?)',
        (underlying_address1, underlying_address2, underlying_address3, underlying_address4),  # noqa: E501
    ).fetchone()[0]
    assert result == 4

    # now test that adding a token with underlying tokens adding up to more than 100% is caught
    bad_token = EthereumToken.initialize(
        address=make_ethereum_address(),
        decimals=18,
        name='foo',
        symbol='BBB',
        underlying_tokens=[
            UnderlyingToken(address=make_ethereum_address(), weight=FVal('0.5055')),
            UnderlyingToken(address=make_ethereum_address(), weight=FVal('0.7055')),
        ],
    )
    serialized_token = bad_token.serialize_all_info()
    del serialized_token['identifier']
    response = requests.put(
        api_url_for(
            rotkehlchen_api_server,
            'ethereumassetsresource',
        ),
        json={'token': serialized_token},
    )
    expected_msg = (
        f'The sum of underlying token weights for {bad_token.ethereum_address} is '
        f'121.1000 and exceeds 100%'
    )
    assert_error_response(
        response=response,
        contained_in_msg=expected_msg,
        status_code=HTTPStatus.BAD_REQUEST,
    )
    # and test that adding a token with underlying tokens adding up to less than 100% is caught
    bad_token = EthereumToken.initialize(
        address=make_ethereum_address(),
        decimals=18,
        name='foo',
        symbol='BBB',
        underlying_tokens=[
            UnderlyingToken(address=make_ethereum_address(), weight=FVal('0.1055')),
            UnderlyingToken(address=make_ethereum_address(), weight=FVal('0.2055')),
        ],
    )
    serialized_token = bad_token.serialize_all_info()
    del serialized_token['identifier']
    response = requests.put(
        api_url_for(
            rotkehlchen_api_server,
            'ethereumassetsresource',
        ),
        json={'token': serialized_token},
    )
    expected_msg = (
        f'The sum of underlying token weights for {bad_token.ethereum_address} is '
        f'31.1000 and does not add up to 100%'
    )
    assert_error_response(
        response=response,
        contained_in_msg=expected_msg,
        status_code=HTTPStatus.BAD_REQUEST,
    )
    # and test that adding a token with empty list of underlying tokens and not null is an error
    bad_token = EthereumToken.initialize(
        address=make_ethereum_address(),
        decimals=18,
        name='foo',
        symbol='BBB',
        underlying_tokens=[],
    )
    serialized_bad_token = bad_token.serialize_all_info()
    del serialized_bad_token['identifier']
    serialized_bad_token['underlying_tokens'] = []
    response = requests.put(
        api_url_for(
            rotkehlchen_api_server,
            'ethereumassetsresource',
        ),
        json={'token': serialized_bad_token},
    )
    expected_msg = (
        f'Gave an empty list for underlying tokens of {bad_token.ethereum_address}'
    )
    assert_error_response(
        response=response,
        contained_in_msg=expected_msg,
        status_code=HTTPStatus.BAD_REQUEST,
    )
    # test that adding invalid coingecko fails
    bad_identifier = 'INVALIDID'
    bad_token = {
        'address': make_ethereum_address(),
        'decimals': 18,
        'name': 'Bad token',
        'symbol': 'NAUGHTY',
        'coingecko': bad_identifier,
    }
    response = requests.put(
        api_url_for(
            rotkehlchen_api_server,
            'ethereumassetsresource',
        ),
        json={'token': bad_token},
    )
    assert_error_response(
        response=response,
        contained_in_msg=f'Given coingecko identifier {bad_identifier} is not valid',
        status_code=HTTPStatus.BAD_REQUEST,
    )
    # test that adding invalid cryptocompare fails
    bad_token['cryptocompare'] = bad_identifier
    bad_token['coingecko'] = None
    response = requests.put(
        api_url_for(
            rotkehlchen_api_server,
            'ethereumassetsresource',
        ),
        json={'token': bad_token},
    )
    assert_error_response(
        response=response,
        contained_in_msg=f'Given cryptocompare identifier {bad_identifier} isnt valid',
        status_code=HTTPStatus.BAD_REQUEST,
    )
示例#9
0
def deserialize_bpt_event(
        userdb: 'DBHandler',
        raw_event: Dict[str, Any],
        event_type: Literal[BalancerBPTEventType.MINT, BalancerBPTEventType.BURN],
) -> BalancerBPTEvent:
    """May raise DeserializationError"""
    try:
        tx_hash, log_index = deserialize_transaction_id(raw_event['id'])
        raw_user_address = raw_event['user']['id']
        amount = deserialize_asset_amount(raw_event['amount'])
        raw_pool = raw_event['pool']
        raw_pool_address = raw_pool['id']
        raw_pool_tokens = raw_pool['tokens']
        total_weight = deserialize_asset_amount(raw_pool['totalWeight'])
    except KeyError as e:
        raise DeserializationError(f'Missing key: {str(e)}.') from e

    if total_weight == ZERO:
        raise DeserializationError('Pool weight is zero.')

    user_address = deserialize_ethereum_address(raw_user_address)
    pool_address = deserialize_ethereum_address(raw_pool_address)

    underlying_tokens = []
    for raw_token in raw_pool_tokens:
        try:
            raw_token_address = raw_token['address']
            token_symbol = raw_token['symbol']
            token_name = raw_token['name']
            token_decimals = raw_token['decimals']
            token_weight = deserialize_asset_amount(raw_token['denormWeight'])
        except KeyError as e:
            raise DeserializationError(f'Missing key: {str(e)}.') from e

        token_address = deserialize_ethereum_address(raw_token_address)

        token = get_or_create_ethereum_token(
            userdb=userdb,
            symbol=token_symbol,
            ethereum_address=token_address,
            name=token_name,
            decimals=token_decimals,
        )
        underlying_tokens.append(UnderlyingToken(
            address=token.ethereum_address,
            weight=token_weight / total_weight,
        ))

    underlying_tokens.sort(key=lambda x: x.address)
    pool_address_token = get_or_create_ethereum_token(
        userdb=userdb,
        ethereum_address=pool_address,
        symbol='BPT',
        protocol='balancer',
        decimals=18,  # all BPT tokens have 18 decimals
        underlying_tokens=underlying_tokens,
        form_with_incomplete_data=True,  # since some may not have decimals input correctly
    )
    bpt_event = BalancerBPTEvent(
        tx_hash=tx_hash,
        log_index=log_index,
        address=user_address,
        event_type=event_type,
        pool_address_token=pool_address_token,
        amount=amount,
    )
    return bpt_event
示例#10
0
def deserialize_pool_share(
        userdb: 'DBHandler',
        raw_pool_share: Dict[str, Any],
) -> Tuple[ChecksumEthAddress, BalancerPoolBalance]:
    """May raise DeserializationError"""
    try:
        raw_user_address = raw_pool_share['userAddress']['id']
        user_amount = deserialize_asset_amount(raw_pool_share['balance'])
        raw_pool = raw_pool_share['poolId']
        total_amount = deserialize_asset_amount(raw_pool['totalShares'])
        raw_address = raw_pool['id']
        raw_tokens = raw_pool['tokens']
        total_weight = deserialize_asset_amount(raw_pool['totalWeight'])
    except KeyError as e:
        raise DeserializationError(f'Missing key: {str(e)}.') from e

    if total_weight == ZERO:
        raise DeserializationError('Pool weight is zero.')

    user_address = deserialize_ethereum_address(raw_user_address)
    pool_address = deserialize_ethereum_address(raw_address)

    pool_tokens = []
    pool_token_balances = []
    for raw_token in raw_tokens:
        try:
            raw_token_address = raw_token['address']
            token_symbol = raw_token['symbol']
            token_name = raw_token['name']
            token_decimals = raw_token['decimals']
            token_total_amount = deserialize_asset_amount(raw_token['balance'])
            token_weight = deserialize_asset_amount(raw_token['denormWeight'])
        except KeyError as e:
            raise DeserializationError(f'Missing key: {str(e)}.') from e

        token_address = deserialize_ethereum_address(raw_token_address)

        token = get_or_create_ethereum_token(
            userdb=userdb,
            symbol=token_symbol,
            ethereum_address=token_address,
            name=token_name,
            decimals=token_decimals,
        )
        if token_total_amount == ZERO:
            raise DeserializationError(f'Token {token.identifier} balance is zero.')

        weight = token_weight * 100 / total_weight
        token_user_amount = user_amount / total_amount * token_total_amount
        pool_token_balance = BalancerPoolTokenBalance(
            token=token,
            total_amount=token_total_amount,
            user_balance=Balance(amount=token_user_amount),
            weight=weight,
        )
        pool_token_balances.append(pool_token_balance)
        pool_token = UnderlyingToken(address=token.ethereum_address, weight=weight / 100)
        pool_tokens.append(pool_token)

    pool_tokens.sort(key=lambda x: x.address)
    pool_token_balances.sort(key=lambda x: x.token.ethereum_address)
    balancer_pool_token = get_or_create_ethereum_token(
        userdb=userdb,
        symbol='BPT',
        ethereum_address=pool_address,
        protocol='balancer',
        decimals=18,  # All BPT tokens have 18 decimals
        underlying_tokens=pool_tokens,
        form_with_incomplete_data=True,  # since some may not have had decimals input correctly
    )
    pool = BalancerPoolBalance(
        pool_token=balancer_pool_token,
        underlying_tokens_balance=pool_token_balances,
        total_amount=total_amount,
        user_balance=Balance(amount=user_amount),
    )
    return user_address, pool