exclude CELO native token balance from net worth

This commit is contained in:
tom
2025-08-25 09:44:03 +02:00
parent ed384d70f9
commit 53eca5cc27
20 changed files with 216 additions and 18 deletions
+1
View File
@@ -14,6 +14,7 @@ on:
- arbitrum_nova
- arbitrum_sepolia
- base
- celo
- celo_alfajores
- garnet
- gnosis
+1
View File
@@ -14,6 +14,7 @@ on:
- arbitrum_nova
- arbitrum_sepolia
- base
- celo
- celo_alfajores
- garnet
- gnosis
+1
View File
@@ -364,6 +364,7 @@
"arbitrum_sepolia",
"base",
"blackfort_testnet",
"celo",
"celo_alfajores",
"garnet",
"gnosis",
+2 -1
View File
@@ -4,12 +4,13 @@ import { getEnvValue } from '../utils';
const title = 'Celo chain';
const config: Feature<{ }> = (() => {
const config: Feature<{ nativeTokenAddress?: string }> = (() => {
if (getEnvValue('NEXT_PUBLIC_CELO_ENABLED') === 'true') {
return Object.freeze({
title,
isEnabled: true,
nativeTokenAddress: getEnvValue('NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS'),
});
}
+66
View File
@@ -0,0 +1,66 @@
# Set of ENVs for Celo Mainnet network explorer
# https://celo.blockscout.com
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=celo"
# Local ENVs
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS=0x471EcE3750Da237f93B8E339c536989b8978a438
# Instance ENVs
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS=['talentprotocol', 'efp', 'webacy', 'deepdao', 'humanpassport', 'bankless', 'blockscoutbadges']
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/widgets/config.json
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=celo.blockscout.com
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_CELO_ENABLED=true
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Revokescout','icon':'integration/partial','dappId':'revokescout'}]
NEXT_PUBLIC_DEX_POOLS_ENABLED=true
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/celo.json
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
NEXT_PUBLIC_GAS_TRACKER_ENABLED=false
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xa214ff978b8535434d5806d8e9d0d5f8c946905fbba1def121014e7af36c1e8f
NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(252, 255, 82, 1)'],'text_color':['rgba(0, 0, 0, 1)']}
NEXT_PUBLIC_HOMEPAGE_STATS=['total_blocks','average_block_time','total_txs','wallet_addresses','current_epoch']
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/b27a4aecaad513fa033e37430a4f9a47/raw/3a2fa70068ea27c3e6d58dc4cdbeb732968d62f3/revokescout-banner.html
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://revoke.blockscout.com?utm_source=blockscout&utm_medium=celo
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY=patbqG4V2CI998jAq.9810c58c9de973ba2650621c94559088cbdfa1a914498e385621ed035d33c0d0
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=CELO
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=CELO
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/celo-icon-light.svg
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/celo-icon-dark.svg
NEXT_PUBLIC_NETWORK_ID=42220
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/celo-logo-light.svg
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/celo-logo-dark.svg
NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES=true
NEXT_PUBLIC_NETWORK_NAME=Celo Mainnet
NEXT_PUBLIC_NETWORK_RPC_URL=https://forno.celo.org
NEXT_PUBLIC_NETWORK_SHORT_NAME=Mainnet
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/celo.png
NEXT_PUBLIC_PUZZLE_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/capyPuzzleBadge
NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service
NEXT_PUBLIC_STATS_API_HOST=https://celo.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees']
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
+10
View File
@@ -364,6 +364,16 @@ const celoSchema = yup
.object()
.shape({
NEXT_PUBLIC_CELO_ENABLED: yup.boolean(),
NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS: yup
.string()
.min(42)
.max(42)
.matches(regexp.HEX_REGEXP_WITH_0X)
.when('NEXT_PUBLIC_CELO_ENABLED', {
is: (value: boolean) => value,
then: (schema) => schema,
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS can only be used if NEXT_PUBLIC_CELO_ENABLED is set to \'true\''),
}),
});
const apiDocsScheme = yup
+2 -1
View File
@@ -1 +1,2 @@
NEXT_PUBLIC_CELO_ENABLED=true
NEXT_PUBLIC_CELO_ENABLED=true
NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS=0x471EcE3750Da237f93B8E339c536989b8978a438
+1
View File
@@ -792,6 +792,7 @@ For blockchains that use the Celo platform. _Note_, that once the Celo mainnet b
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_CELO_ENABLED | `boolean` | Indicates that it is a Celo-based chain. | - | - | `true` | v1.37.0+ |
| NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS | `string` | The address of the CELO ERC-20 token. Used to exclude its balance from the net worth value of user tokens. | - | - | `0x471EcE3750Da237f93B8E339c536989b8978a438` | v2.3.0+ |
&nbsp;
+1
View File
@@ -112,6 +112,7 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
],
celo: [
[ 'NEXT_PUBLIC_CELO_ENABLED', 'true' ],
[ 'NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS', '0x471EcE3750Da237f93B8E339c536989b8978a438' ],
],
opSuperchain: [
[ 'NEXT_PUBLIC_OP_SUPERCHAIN_ENABLED', 'true' ],
+1
View File
@@ -7,6 +7,7 @@ const PRESETS = {
arbitrum_sepolia: 'https://arbitrum-sepolia.blockscout.com',
base: 'https://base.blockscout.com',
blackfort_testnet: 'https://blackfort-testnet.blockscout.com',
celo: 'https://celo.blockscout.com',
celo_alfajores: 'https://celo-alfajores.blockscout.com',
eth: 'https://eth.blockscout.com',
eth_goerli: 'https://eth-goerli.blockscout.com',
+23
View File
@@ -5,6 +5,7 @@ import type { AddressTokensResponse } from 'types/api/address';
import * as addressMock from 'mocks/address/address';
import * as tokensMock from 'mocks/address/tokens';
import * as tokenInfo from 'mocks/tokens/tokenInfo';
import * as tokenInstance from 'mocks/tokens/tokenInstance';
import * as socketServer from 'playwright/fixtures/socketServer';
import { test, expect, devices } from 'playwright/lib';
@@ -268,3 +269,25 @@ test.describe('update balances via socket', () => {
await expect(component).toHaveScreenshot();
});
});
test('native token', async({ render, mockEnvs }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_CELO_ENABLED', 'true' ],
[ 'NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS', tokenInfo.tokenInfoERC20c.address_hash ],
]);
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_erc20' },
isReady: true,
},
};
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokens/>
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

+20 -1
View File
@@ -3,7 +3,7 @@ import React from 'react';
import * as addressMock from 'mocks/address/address';
import * as tokensMock from 'mocks/address/tokens';
import { tokenInfoERC20a } from 'mocks/tokens/tokenInfo';
import { tokenInfoERC20c, tokenInfoERC20a } from 'mocks/tokens/tokenInfo';
import { test, expect, devices } from 'playwright/lib';
import MockAddressPage from 'ui/address/testUtils/MockAddressPage';
@@ -126,3 +126,22 @@ test('long values', async({ render, page, mockApiResponse }) => {
await expect(page).toHaveScreenshot({ clip: CLIPPING_AREA });
});
test('native token', async({ render, mockEnvs, page }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_CELO_ENABLED', 'true' ],
[ 'NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS', tokenInfoERC20c.address_hash ],
]);
await render(
<MockAddressPage>
<Flex>
<TokenSelect/>
</Flex>
</MockAddressPage>,
{ hooksConfig },
);
await page.getByRole('button', { name: /select/i }).click();
await expect(page).toHaveScreenshot({ clip: CLIPPING_AREA });
});
+17 -2
View File
@@ -4,19 +4,25 @@ import React from 'react';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import getCurrencyValue from 'lib/getCurrencyValue';
import { Link } from 'toolkit/chakra/link';
import NativeTokenTag from 'ui/shared/celo/NativeTokenTag';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import TruncatedValue from 'ui/shared/TruncatedValue';
import type { TokenEnhancedData } from '../utils/tokenUtils';
const celoFeature = config.features.celo;
interface Props {
data: TokenEnhancedData;
}
const TokenSelectItem = ({ data }: Props) => {
const isNativeToken = celoFeature.isEnabled && data.token.address_hash.toLowerCase() === celoFeature.nativeTokenAddress?.toLowerCase();
const secondRow = (() => {
switch (data.token.type) {
case 'ERC-20': {
@@ -93,13 +99,22 @@ const TokenSelectItem = ({ data }: Props) => {
noCopy
noLink
fontWeight={ 700 }
width="auto"
mr={ 2 }
/>
{ isNativeToken && <NativeTokenTag mr={ 2 }/> }
{ data.usd && (
<TruncatedValue value={ `$${ data.usd.toFormat(2) }` } fontWeight={ 700 } minW="120px" ml="auto" textAlign="right"/>
<TruncatedValue
value={ `$${ data.usd.toFormat(2) }` }
fontWeight={ 700 }
minW="120px"
ml="auto"
textAlign="right"
color={ isNativeToken ? 'text.secondary' : undefined }
/>
) }
</Flex>
<Flex alignItems="center" justifyContent="space-between" w="100%" whiteSpace="nowrap">
<Flex alignItems="center" justifyContent="space-between" w="100%" whiteSpace="nowrap" color={ isNativeToken ? 'text.secondary' : undefined }>
{ secondRow }
</Flex>
</Link>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+9 -1
View File
@@ -3,13 +3,17 @@ import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import config from 'configs/app';
import getCurrencyValue from 'lib/getCurrencyValue';
import { Skeleton } from 'toolkit/chakra/skeleton';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import NativeTokenTag from 'ui/shared/celo/NativeTokenTag';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
const celoFeature = config.features.celo;
type Props = AddressTokenBalance & { isLoading: boolean };
const ERC20TokensListItem = ({ token, value, isLoading }: Props) => {
@@ -19,16 +23,20 @@ const ERC20TokensListItem = ({ token, value, isLoading }: Props) => {
usd: tokenValue,
} = getCurrencyValue({ value: value, exchangeRate: token.exchange_rate, decimals: token.decimals, accuracy: 8, accuracyUsd: 2 });
const isNativeToken = celoFeature.isEnabled && token.address_hash.toLowerCase() === celoFeature.nativeTokenAddress?.toLowerCase();
return (
<ListItemMobile rowGap={ 2 }>
<Flex alignItems="center" width="100%">
<Flex alignItems="center" width="100%" columnGap={ 2 }>
<TokenEntity
token={ token }
isLoading={ isLoading }
noCopy
jointSymbol
fontWeight="700"
width="auto"
/>
{ isNativeToken && <NativeTokenTag/> }
</Flex>
<Flex alignItems="center" pl={ 8 }>
<AddressEntity
+21 -11
View File
@@ -1,17 +1,21 @@
import { Flex } from '@chakra-ui/react';
import { Flex, HStack } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import config from 'configs/app';
import getCurrencyValue from 'lib/getCurrencyValue';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { TableCell, TableRow } from 'toolkit/chakra/table';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import NativeTokenTag from 'ui/shared/celo/NativeTokenTag';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
type Props = AddressTokenBalance & { isLoading: boolean };
const celoFeature = config.features.celo;
const ERC20TokensTableItem = ({
token,
value,
@@ -23,16 +27,22 @@ const ERC20TokensTableItem = ({
usd: tokenValue,
} = getCurrencyValue({ value: value, exchangeRate: token.exchange_rate, decimals: token.decimals, accuracy: 8, accuracyUsd: 2 });
const isNativeToken = celoFeature.isEnabled && token.address_hash.toLowerCase() === celoFeature.nativeTokenAddress?.toLowerCase();
return (
<TableRow role="group" >
<TableCell verticalAlign="middle">
<TokenEntity
token={ token }
isLoading={ isLoading }
noCopy
jointSymbol
fontWeight="700"
/>
<HStack gap={ 2 }>
<TokenEntity
token={ token }
isLoading={ isLoading }
noCopy
jointSymbol
fontWeight="700"
width="auto"
/>
{ isNativeToken && <NativeTokenTag/> }
</HStack>
</TableCell>
<TableCell verticalAlign="middle">
<Flex alignItems="center" width="150px" justifyContent="space-between">
@@ -46,17 +56,17 @@ const ERC20TokensTableItem = ({
</Flex>
</TableCell>
<TableCell isNumeric verticalAlign="middle">
<Skeleton loading={ isLoading } display="inline-block">
<Skeleton loading={ isLoading } display="inline-block" color={ isNativeToken ? 'text.secondary' : undefined }>
{ token.exchange_rate && `$${ Number(token.exchange_rate).toLocaleString() }` }
</Skeleton>
</TableCell>
<TableCell isNumeric verticalAlign="middle">
<Skeleton loading={ isLoading } display="inline">
<Skeleton loading={ isLoading } display="inline" color={ isNativeToken ? 'text.secondary' : undefined }>
{ tokenQuantity }
</Skeleton>
</TableCell>
<TableCell isNumeric verticalAlign="middle">
<Skeleton loading={ isLoading } display="inline">
<Skeleton loading={ isLoading } display="inline" color={ isNativeToken ? 'text.secondary' : undefined }>
{ tokenValue && `$${ tokenValue }` }
</Skeleton>
</TableCell>
+7 -1
View File
@@ -3,9 +3,15 @@ import BigNumber from 'bignumber.js';
import type { AddressTokenBalance } from 'types/api/address';
import type { TokenType } from 'types/api/token';
import config from 'configs/app';
import sumBnReducer from 'lib/bigint/sumBnReducer';
import { ZERO } from 'toolkit/utils/consts';
const celoFeature = config.features.celo;
const isNativeToken = (token: TokenEnhancedData) =>
celoFeature.isEnabled && token.token.address_hash.toLowerCase() === celoFeature.nativeTokenAddress?.toLowerCase();
export type TokenEnhancedData = AddressTokenBalance & {
usd?: BigNumber ;
};
@@ -94,7 +100,7 @@ export const calculateUsdValue = (data: AddressTokenBalance): TokenEnhancedData
export const getTokensTotalInfo = (data: TokenSelectData) => {
const usd = Object.values(data)
.map(({ items }) => items.reduce(usdValueReducer, ZERO))
.map(({ items }) => items.filter((item) => !isNativeToken(item)).reduce(usdValueReducer, ZERO))
.reduce(sumBnReducer, ZERO);
const num = Object.values(data)
+33
View File
@@ -0,0 +1,33 @@
import React from 'react';
import config from 'configs/app';
import { Link } from 'toolkit/chakra/link';
import { Tag, type TagProps } from 'toolkit/chakra/tag';
import { Tooltip } from 'toolkit/chakra/tooltip';
const feature = config.features.celo;
interface Props extends TagProps {}
const NativeTokenTag = (props: Props) => {
if (!feature.isEnabled || !feature.nativeTokenAddress) {
return null;
}
const tooltipContent = (
<>
<span>This ERC-20 token represents the native CELO balance for this address and isn’t counted twice. </span>
<Link href="https://docs.celo.org/what-is-celo/using-celo/protocol/celo-token" external>
Learn more
</Link>
</>
);
return (
<Tooltip content={ tooltipContent } interactive>
<Tag { ...props }>Native token</Tag>
</Tooltip>
);
};
export default React.memo(NativeTokenTag);