Commit 7141aa3c authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into tom2drum/issue-2029

parents a67f0714 dc83febb
...@@ -37,6 +37,7 @@ import type { ...@@ -37,6 +37,7 @@ import type {
AddressMudRecordsFilter, AddressMudRecordsFilter,
AddressMudRecordsSorting, AddressMudRecordsSorting,
AddressMudRecord, AddressMudRecord,
AddressEpochRewardsResponse,
} from 'types/api/address'; } from 'types/api/address';
import type { AddressesResponse, AddressesMetadataSearchResult, AddressesMetadataSearchFilters } from 'types/api/addresses'; import type { AddressesResponse, AddressesMetadataSearchResult, AddressesMetadataSearchFilters } from 'types/api/addresses';
import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata'; import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata';
...@@ -522,6 +523,11 @@ export const RESOURCES = { ...@@ -522,6 +523,11 @@ export const RESOURCES = {
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
filterFields: [], filterFields: [],
}, },
address_epoch_rewards: {
path: '/api/v2/addresses/:hash/election-rewards',
pathParams: [ 'hash' as const ],
filterFields: [],
},
// CONTRACT // CONTRACT
contract: { contract: {
...@@ -1019,7 +1025,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward ...@@ -1019,7 +1025,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward
'addresses' | 'addresses_metadata_search' | 'addresses' | 'addresses_metadata_search' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' | 'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search' | 'search' |
'address_logs' | 'address_tokens' | 'address_nfts' | 'address_collections' | 'address_logs' | 'address_tokens' | 'address_nfts' | 'address_collections' | 'address_epoch_rewards' |
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' | 'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' |
'token_instance_transfers' | 'token_instance_holders' | 'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' | 'verified_contracts' |
...@@ -1199,6 +1205,7 @@ Q extends 'address_mud_tables' ? AddressMudTables : ...@@ -1199,6 +1205,7 @@ Q extends 'address_mud_tables' ? AddressMudTables :
Q extends 'address_mud_tables_count' ? number : Q extends 'address_mud_tables_count' ? number :
Q extends 'address_mud_records' ? AddressMudRecords : Q extends 'address_mud_records' ? AddressMudRecords :
Q extends 'address_mud_record' ? AddressMudRecord : Q extends 'address_mud_record' ? AddressMudRecord :
Q extends 'address_epoch_rewards' ? AddressEpochRewardsResponse :
Q extends 'withdrawals' ? WithdrawalsResponse : Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters : Q extends 'withdrawals_counters' ? WithdrawalsCounters :
never; never;
......
import type { AddressEpochRewardsResponse } from 'types/api/address';
import { tokenInfo } from 'mocks/tokens/tokenInfo';
import { withEns, withName, withoutName } from './address';
export const epochRewards: AddressEpochRewardsResponse = {
items: [
{
type: 'delegated_payment',
amount: '136609473658452408568',
account: withName,
associated_account: withName,
block_hash: '0x',
block_number: 26369280,
epoch_number: 1526,
token: tokenInfo,
},
{
type: 'group',
amount: '117205842355246195095',
account: withoutName,
associated_account: withoutName,
block_hash: '0x',
block_number: 26352000,
epoch_number: 1525,
token: tokenInfo,
},
{
type: 'validator',
amount: '125659647325556554060',
account: withEns,
associated_account: withEns,
block_hash: '0x',
block_number: 26300160,
epoch_number: 1524,
token: tokenInfo,
},
],
next_page_params: null,
};
...@@ -102,3 +102,14 @@ export const noteTag: AddressMetadataTagApi = { ...@@ -102,3 +102,14 @@ export const noteTag: AddressMetadataTagApi = {
data: '<b>Warning!</b> This is scam! See the <a href="https://example.com" target="_blank">report</a>', data: '<b>Warning!</b> This is scam! See the <a href="https://example.com" target="_blank">report</a>',
}, },
}; };
export const noteTag2: AddressMetadataTagApi = {
slug: 'note0',
name: 'note_0',
tagType: 'note',
ordinal: 0,
meta: {
alertStatus: 'info',
data: 'The token MILF was launched on May 13, 2021. The maximum total supply of the token is 100 billion.',
},
};
...@@ -3,6 +3,7 @@ import type { ...@@ -3,6 +3,7 @@ import type {
AddressCoinBalanceHistoryItem, AddressCoinBalanceHistoryItem,
AddressCollection, AddressCollection,
AddressCounters, AddressCounters,
AddressEpochRewardsItem,
AddressMudTableItem, AddressMudTableItem,
AddressNFT, AddressNFT,
AddressTabsCounters, AddressTabsCounters,
...@@ -10,7 +11,7 @@ import type { ...@@ -10,7 +11,7 @@ import type {
} from 'types/api/address'; } from 'types/api/address';
import type { AddressesItem } from 'types/api/addresses'; import type { AddressesItem } from 'types/api/addresses';
import { ADDRESS_HASH } from './addressParams'; import { ADDRESS_HASH, ADDRESS_PARAMS } from './addressParams';
import { MUD_SCHEMA, MUD_TABLE } from './mud'; import { MUD_SCHEMA, MUD_TABLE } from './mud';
import { TOKEN_INFO_ERC_1155, TOKEN_INFO_ERC_20, TOKEN_INFO_ERC_721, TOKEN_INFO_ERC_404, TOKEN_INSTANCE } from './token'; import { TOKEN_INFO_ERC_1155, TOKEN_INFO_ERC_20, TOKEN_INFO_ERC_721, TOKEN_INFO_ERC_404, TOKEN_INSTANCE } from './token';
import { TX_HASH } from './tx'; import { TX_HASH } from './tx';
...@@ -116,3 +117,14 @@ export const ADDRESS_MUD_TABLE_ITEM: AddressMudTableItem = { ...@@ -116,3 +117,14 @@ export const ADDRESS_MUD_TABLE_ITEM: AddressMudTableItem = {
schema: MUD_SCHEMA, schema: MUD_SCHEMA,
table: MUD_TABLE, table: MUD_TABLE,
}; };
export const EPOCH_REWARD_ITEM: AddressEpochRewardsItem = {
amount: '136609473658452408568',
block_number: 10355938,
type: 'voter',
token: TOKEN_INFO_ERC_20,
block_hash: '0x5956a847d8089e254e02e5111cad6992b99ceb9e5c2dc4343fd53002834c4dc6',
account: ADDRESS_PARAMS,
epoch_number: 1526,
associated_account: ADDRESS_PARAMS,
};
...@@ -154,33 +154,34 @@ const variantSubtle = defineStyle((props) => { ...@@ -154,33 +154,34 @@ const variantSubtle = defineStyle((props) => {
// for buttons in the hero banner // for buttons in the hero banner
const variantHero = defineStyle((props) => { const variantHero = defineStyle((props) => {
const buttonConfig = config.UI.homepage.heroBanner?.button;
return { return {
bg: mode( bg: mode(
config.UI.homepage.heroBanner?.button?._default?.background?.[0] || 'blue.600', buttonConfig?._default?.background?.[0] || 'blue.600',
config.UI.homepage.heroBanner?.button?._default?.background?.[1] || 'blue.600', buttonConfig?._default?.background?.[1] || buttonConfig?._default?.background?.[0] || 'blue.600',
)(props), )(props),
color: mode( color: mode(
config.UI.homepage.heroBanner?.button?._default?.text_color?.[0] || 'white', buttonConfig?._default?.text_color?.[0] || 'white',
config.UI.homepage.heroBanner?.button?._default?.text_color?.[1] || 'white', buttonConfig?._default?.text_color?.[1] || buttonConfig?._default?.text_color?.[0] || 'white',
)(props), )(props),
_hover: { _hover: {
bg: mode( bg: mode(
config.UI.homepage.heroBanner?.button?._hover?.background?.[0] || 'blue.400', buttonConfig?._hover?.background?.[0] || 'blue.400',
config.UI.homepage.heroBanner?.button?._hover?.background?.[1] || 'blue.400', buttonConfig?._hover?.background?.[1] || buttonConfig?._hover?.background?.[0] || 'blue.400',
)(props), )(props),
color: mode( color: mode(
config.UI.homepage.heroBanner?.button?._hover?.text_color?.[0] || 'white', buttonConfig?._hover?.text_color?.[0] || 'white',
config.UI.homepage.heroBanner?.button?._hover?.text_color?.[1] || 'white', buttonConfig?._hover?.text_color?.[1] || buttonConfig?._hover?.text_color?.[0] || 'white',
)(props), )(props),
}, },
'&[data-selected=true]': { '&[data-selected=true]': {
bg: mode( bg: mode(
config.UI.homepage.heroBanner?.button?._selected?.background?.[0] || 'blue.50', buttonConfig?._selected?.background?.[0] || 'blue.50',
config.UI.homepage.heroBanner?.button?._selected?.background?.[1] || 'blue.50', buttonConfig?._selected?.background?.[1] || buttonConfig?._selected?.background?.[0] || 'blue.50',
)(props), )(props),
color: mode( color: mode(
config.UI.homepage.heroBanner?.button?._selected?.text_color?.[0] || 'blackAlpha.800', buttonConfig?._selected?.text_color?.[0] || 'blackAlpha.800',
config.UI.homepage.heroBanner?.button?._selected?.text_color?.[1] || 'blackAlpha.800', buttonConfig?._selected?.text_color?.[1] || buttonConfig?._selected?.text_color?.[0] || 'blackAlpha.800',
)(props), )(props),
}, },
}; };
......
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
import type { UserTags, AddressImplementation } from './addressParams'; import type { UserTags, AddressImplementation, AddressParam } from './addressParams';
import type { Block } from './block'; import type { Block, EpochRewardsType } from './block';
import type { InternalTransaction } from './internalTransaction'; import type { InternalTransaction } from './internalTransaction';
import type { MudWorldSchema, MudWorldTable } from './mudWorlds'; import type { MudWorldSchema, MudWorldTable } from './mudWorlds';
import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token'; import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token';
...@@ -191,6 +191,7 @@ export type AddressTabsCounters = { ...@@ -191,6 +191,7 @@ export type AddressTabsCounters = {
transactions_count: number | null; transactions_count: number | null;
validations_count: number | null; validations_count: number | null;
withdrawals_count: number | null; withdrawals_count: number | null;
celo_election_rewards_count?: number | null;
} }
// MUD framework // MUD framework
...@@ -245,3 +246,25 @@ export type AddressMudRecord = { ...@@ -245,3 +246,25 @@ export type AddressMudRecord = {
schema: MudWorldSchema; schema: MudWorldSchema;
table: MudWorldTable; table: MudWorldTable;
} }
export type AddressEpochRewardsResponse = {
items: Array<AddressEpochRewardsItem>;
next_page_params: {
amount: string;
associated_account_address_hash: string;
block_number: number;
items_count: number;
type: EpochRewardsType;
} | null;
}
export type AddressEpochRewardsItem = {
type: EpochRewardsType;
token: TokenInfo;
amount: string;
block_number: number;
block_hash: string;
account: AddressParam;
epoch_number: number;
associated_account: AddressParam;
}
...@@ -144,6 +144,8 @@ export interface BlockEpochElectionReward { ...@@ -144,6 +144,8 @@ export interface BlockEpochElectionReward {
total: string; total: string;
} }
export type EpochRewardsType = 'group' | 'validator' | 'delegated_payment' | 'voter';
export interface BlockEpoch { export interface BlockEpoch {
number: number; number: number;
distribution: { distribution: {
...@@ -151,12 +153,7 @@ export interface BlockEpoch { ...@@ -151,12 +153,7 @@ export interface BlockEpoch {
community_transfer: TokenTransfer | null; community_transfer: TokenTransfer | null;
reserve_bolster_transfer: TokenTransfer | null; reserve_bolster_transfer: TokenTransfer | null;
}; };
aggregated_election_rewards: { aggregated_election_rewards: Record<EpochRewardsType, BlockEpochElectionReward | null>;
delegated_payment: BlockEpochElectionReward | null;
group: BlockEpochElectionReward | null;
validator: BlockEpochElectionReward | null;
voter: BlockEpochElectionReward | null;
};
} }
export interface BlockEpochElectionRewardDetails { export interface BlockEpochElectionRewardDetails {
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import { epochRewards } from 'mocks/address/epochRewards';
import { test, expect } from 'playwright/lib';
import AddressEpochRewards from './AddressEpochRewards';
const ADDRESS_HASH = '0x1234';
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH },
},
};
test('base view +@mobile', async({ render, mockApiResponse }) => {
await mockApiResponse('address_epoch_rewards', epochRewards, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressEpochRewards/>
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
import { Hide, Show } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString';
import { EPOCH_REWARD_ITEM } from 'stubs/address';
import { generateListStub } from 'stubs/utils';
import AddressEpochRewardsTable from 'ui/address/epochRewards/AddressEpochRewardsTable';
import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import AddressEpochRewardsListItem from './epochRewards/AddressEpochRewardsListItem';
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
isQueryEnabled?: boolean;
}
const AddressEpochRewards = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => {
const router = useRouter();
const isMounted = useIsMounted();
const hash = getQueryParamString(router.query.hash);
const rewardsQuery = useQueryWithPages({
resourceName: 'address_epoch_rewards',
pathParams: {
hash,
},
scrollRef,
options: {
enabled: isQueryEnabled && Boolean(hash),
placeholderData: generateListStub<'address_epoch_rewards'>(EPOCH_REWARD_ITEM, 50, { next_page_params: {
amount: '1',
items_count: 50,
type: 'voter',
associated_account_address_hash: '1',
block_number: 10355938,
} }),
},
});
if (!isMounted || !shouldRender) {
return null;
}
const content = rewardsQuery.data?.items ? (
<>
<Hide below="lg" ssr={ false }>
<AddressEpochRewardsTable
items={ rewardsQuery.data.items }
top={ rewardsQuery.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }
isLoading={ rewardsQuery.isPlaceholderData }
/>
</Hide>
<Show below="lg" ssr={ false }>
{ rewardsQuery.data.items.map((item, index) => (
<AddressEpochRewardsListItem
key={ item.block_hash + item.type + item.account.hash + item.associated_account.hash + (rewardsQuery.isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ rewardsQuery.isPlaceholderData }
/>
)) }
</Show>
</>
) : null;
const actionBar = rewardsQuery.pagination.isVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...rewardsQuery.pagination }/>
</ActionBar>
) : null;
return (
<DataListDisplay
isError={ rewardsQuery.isError }
items={ rewardsQuery.data?.items }
emptyText="There are no epoch rewards for this address."
content={ content }
actionBar={ actionBar }
/>
);
};
export default AddressEpochRewards;
import { Flex, Skeleton } from '@chakra-ui/react'; import { Text, Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -45,7 +45,9 @@ const AddressBlocksValidatedListItem = (props: Props) => { ...@@ -45,7 +45,9 @@ const AddressBlocksValidatedListItem = (props: Props) => {
</Flex> </Flex>
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Gas used</Skeleton> <Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Gas used</Skeleton>
<Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ BigNumber(props.gas_used || 0).toFormat() }</Skeleton> <Skeleton isLoaded={ !props.isLoading }>
<Text color="text_secondary">{ BigNumber(props.gas_used || 0).toFormat() }</Text>
</Skeleton>
<BlockGasUsed <BlockGasUsed
gasUsed={ props.gas_used } gasUsed={ props.gas_used }
gasLimit={ props.gas_limit } gasLimit={ props.gas_limit }
...@@ -55,7 +57,9 @@ const AddressBlocksValidatedListItem = (props: Props) => { ...@@ -55,7 +57,9 @@ const AddressBlocksValidatedListItem = (props: Props) => {
{ !config.UI.views.block.hiddenFields?.total_reward && !config.features.rollup.isEnabled && ( { !config.UI.views.block.hiddenFields?.total_reward && !config.features.rollup.isEnabled && (
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Reward { currencyUnits.ether }</Skeleton> <Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Reward { currencyUnits.ether }</Skeleton>
<Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ totalReward.toFixed() }</Skeleton> <Skeleton isLoaded={ !props.isLoading }>
<Text color="text_secondary">{ totalReward.toFixed() }</Text>
</Skeleton>
</Flex> </Flex>
) } ) }
</ListItemMobile> </ListItemMobile>
......
...@@ -57,7 +57,7 @@ const AddressBlocksValidatedTableItem = (props: Props) => { ...@@ -57,7 +57,7 @@ const AddressBlocksValidatedTableItem = (props: Props) => {
</Flex> </Flex>
</Td> </Td>
{ !config.UI.views.block.hiddenFields?.total_reward && !config.features.rollup.isEnabled && ( { !config.UI.views.block.hiddenFields?.total_reward && !config.features.rollup.isEnabled && (
<Td isNumeric display="flex" justifyContent="end"> <Td isNumeric>
<Skeleton isLoaded={ !props.isLoading } display="inline-block"> <Skeleton isLoaded={ !props.isLoading } display="inline-block">
<span>{ totalReward.toFixed() }</span> <span>{ totalReward.toFixed() }</span>
</Skeleton> </Skeleton>
......
...@@ -6,7 +6,7 @@ import { test, expect } from 'playwright/lib'; ...@@ -6,7 +6,7 @@ import { test, expect } from 'playwright/lib';
import AddressMetadataAlert from './AddressMetadataAlert'; import AddressMetadataAlert from './AddressMetadataAlert';
test('base view', async({ render }) => { test('base view', async({ render }) => {
const component = await render(<AddressMetadataAlert tags={ [ metadataMock.noteTag ] }/>); const component = await render(<AddressMetadataAlert tags={ [ metadataMock.noteTag, metadataMock.noteTag2 ] }/>);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
import { Alert, chakra } from '@chakra-ui/react'; import { Alert, Flex, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata'; import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata';
...@@ -9,35 +9,34 @@ interface Props { ...@@ -9,35 +9,34 @@ interface Props {
} }
const AddressMetadataAlert = ({ tags, className }: Props) => { const AddressMetadataAlert = ({ tags, className }: Props) => {
const noteTag = tags?.find(({ tagType }) => tagType === 'note'); const noteTags = tags?.filter(({ tagType }) => tagType === 'note').filter(({ meta }) => meta?.data);
if (!noteTag) {
return null;
}
const content = noteTag.meta?.data;
if (!content) { if (!noteTags?.length) {
return null; return null;
} }
return ( return (
<Alert <Flex flexDir="column" gap={ 3 } className={ className }>
className={ className } { noteTags.map((noteTag) => (
status={ noteTag.meta?.alertStatus ?? 'error' } <Alert
bgColor={ noteTag.meta?.alertBgColor } key={ noteTag.name }
color={ noteTag.meta?.alertTextColor } status={ noteTag.meta?.alertStatus ?? 'error' }
whiteSpace="pre-wrap" bgColor={ noteTag.meta?.alertBgColor }
display="inline-block" color={ noteTag.meta?.alertTextColor }
sx={{ whiteSpace="pre-wrap"
'& a': { display="inline-block"
color: 'link', sx={{
_hover: { '& a': {
color: 'link_hovered', color: 'link',
}, _hover: {
}, color: 'link_hovered',
}} },
dangerouslySetInnerHTML={{ __html: content }} },
/> }}
dangerouslySetInnerHTML={{ __html: noteTag.meta?.data ?? '' }}
/>
)) }
</Flex>
); );
}; };
......
import { Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { AddressEpochRewardsItem } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = {
item: AddressEpochRewardsItem;
isLoading?: boolean;
};
const AddressEpochRewardsListItem = ({ item, isLoading }: Props) => {
const { valueStr } = getCurrencyValue({ value: item.amount, accuracy: 2, decimals: item.token.decimals });
return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto">
<ListItemMobileGrid.Label isLoading={ isLoading }>Block</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<BlockEntity
number={ Number(item.block_number) }
isLoading={ isLoading }
noIcon
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Epoch #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.epoch_number }
</ListItemMobileGrid.Value>
{ /* <ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TimeAgoWithTooltip
timestamp={ item.timestamp }
isLoading={ isLoading }
color="text_secondary"
display="inline-block"
/>
</ListItemMobileGrid.Value> */ }
<ListItemMobileGrid.Label isLoading={ isLoading }>Reward type</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<EpochRewardTypeTag type={ item.type } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Associated address</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<AddressEntity
address={ item.associated_account }
isLoading={ isLoading }
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Value</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="flex" alignItems="center" gap={ 2 }>
{ valueStr }
<TokenEntity token={ item.token } isLoading={ isLoading } onlySymbol width="auto" noCopy/>
</Skeleton>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
);
};
export default AddressEpochRewardsListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { AddressEpochRewardsItem } from 'types/api/address';
import { default as Thead } from 'ui/shared/TheadSticky';
import AddressEpochRewardsTableItem from './AddressEpochRewardsTableItem';
type Props = {
items: Array<AddressEpochRewardsItem>;
isLoading?: boolean;
top: number;
};
const AddressEpochRewardsTable = ({ items, isLoading, top }: Props) => {
return (
<Table variant="simple" size="sm" minW="1000px" style={{ tableLayout: 'auto' }}>
<Thead top={ top }>
<Tr>
<Th>Block</Th>
<Th>Reward type</Th>
<Th>Associated address</Th>
<Th isNumeric>Value</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => {
return (
<AddressEpochRewardsTableItem
key={ item.block_hash + item.type + item.account.hash + item.associated_account.hash + (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
/>
);
}) }
</Tbody>
</Table>
);
};
export default AddressEpochRewardsTable;
import { Flex, Td, Tr, Text, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { AddressEpochRewardsItem } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
type Props = {
item: AddressEpochRewardsItem;
isLoading?: boolean;
};
const AddressEpochRewardsTableItem = ({ item, isLoading }: Props) => {
const { valueStr } = getCurrencyValue({ value: item.amount, decimals: item.token.decimals });
return (
<Tr>
<Td verticalAlign="middle">
<Flex alignItems="center" gap={ 3 }>
<BlockEntity number={ item.block_number } isLoading={ isLoading } noIcon/>
<Text color="text_secondary" fontWeight={ 600 }>{ `Epoch # ${ item.epoch_number }` }</Text>
{ /* no timestamp from API, will be added later */ }
{ /* <TimeAgoWithTooltip timestamp={ item } isLoading={ isLoading }/> */ }
</Flex>
</Td>
<Td verticalAlign="middle">
<EpochRewardTypeTag type={ item.type } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle">
<AddressEntity address={ item.associated_account } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle" isNumeric>
<Skeleton isLoaded={ !isLoading } display="flex" alignItems="center" gap={ 2 } justifyContent="flex-end">
{ valueStr }
<TokenEntity token={ item.token } isLoading={ isLoading } onlySymbol width="auto" noCopy/>
</Skeleton>
</Td>
</Tr>
);
};
export default AddressEpochRewardsTableItem;
import React from 'react';
import type { BlockEpoch } from 'types/api/block';
import Tag from 'ui/shared/chakra/Tag';
interface Props {
type: keyof BlockEpoch['aggregated_election_rewards'];
isLoading?: boolean;
}
const BlockEpochElectionRewardType = ({ type, isLoading }: Props) => {
switch (type) {
case 'delegated_payment':
return <Tag colorScheme="blue" isLoading={ isLoading }>Delegated payments</Tag>;
case 'group':
return <Tag colorScheme="teal" isLoading={ isLoading }>Validator group rewards</Tag>;
case 'validator':
return <Tag colorScheme="purple" isLoading={ isLoading }>Validator rewards</Tag>;
case 'voter':
return <Tag colorScheme="yellow" isLoading={ isLoading }>Voting rewards</Tag>;
}
};
export default React.memo(BlockEpochElectionRewardType);
...@@ -5,10 +5,10 @@ import type { BlockEpoch, BlockEpochElectionReward } from 'types/api/block'; ...@@ -5,10 +5,10 @@ import type { BlockEpoch, BlockEpochElectionReward } from 'types/api/block';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import TokenEntity from 'ui/shared/entities/token/TokenEntity'; import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import BlockEpochElectionRewardDetailsMobile from './BlockEpochElectionRewardDetailsMobile'; import BlockEpochElectionRewardDetailsMobile from './BlockEpochElectionRewardDetailsMobile';
import BlockEpochElectionRewardType from './BlockEpochElectionRewardType';
interface Props { interface Props {
data: BlockEpochElectionReward; data: BlockEpochElectionReward;
...@@ -53,7 +53,7 @@ const BlockEpochElectionRewardsListItem = ({ data, isLoading, type }: Props) => ...@@ -53,7 +53,7 @@ const BlockEpochElectionRewardsListItem = ({ data, isLoading, type }: Props) =>
/> />
</Skeleton> </Skeleton>
) : <Box boxSize={ 6 }/> } ) : <Box boxSize={ 6 }/> }
<BlockEpochElectionRewardType type={ type } isLoading={ isLoading }/> <EpochRewardTypeTag type={ type } isLoading={ isLoading }/>
<Skeleton isLoaded={ !isLoading }>{ data.count }</Skeleton> <Skeleton isLoaded={ !isLoading }>{ data.count }</Skeleton>
<Flex columnGap={ 2 } alignItems="center" ml="auto" fontWeight={ 500 }> <Flex columnGap={ 2 } alignItems="center" ml="auto" fontWeight={ 500 }>
<Skeleton isLoaded={ !isLoading }>{ valueStr }</Skeleton> <Skeleton isLoaded={ !isLoading }>{ valueStr }</Skeleton>
......
...@@ -5,10 +5,10 @@ import type { BlockEpoch, BlockEpochElectionReward } from 'types/api/block'; ...@@ -5,10 +5,10 @@ import type { BlockEpoch, BlockEpochElectionReward } from 'types/api/block';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import TokenEntity from 'ui/shared/entities/token/TokenEntity'; import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import BlockEpochElectionRewardDetailsDesktop from './BlockEpochElectionRewardDetailsDesktop'; import BlockEpochElectionRewardDetailsDesktop from './BlockEpochElectionRewardDetailsDesktop';
import BlockEpochElectionRewardType from './BlockEpochElectionRewardType';
import { getRewardNumText } from './utils'; import { getRewardNumText } from './utils';
interface Props { interface Props {
...@@ -54,7 +54,7 @@ const BlockEpochElectionRewardsTableItem = ({ isLoading, data, type }: Props) => ...@@ -54,7 +54,7 @@ const BlockEpochElectionRewardsTableItem = ({ isLoading, data, type }: Props) =>
) } ) }
</Td> </Td>
<Td borderColor={ mainRowBorderColor }> <Td borderColor={ mainRowBorderColor }>
<BlockEpochElectionRewardType type={ type } isLoading={ isLoading }/> <EpochRewardTypeTag type={ type } isLoading={ isLoading }/>
</Td> </Td>
<Td borderColor={ mainRowBorderColor }> <Td borderColor={ mainRowBorderColor }>
<Skeleton isLoaded={ !isLoading } fontWeight={ 400 } my={ 1 }> <Skeleton isLoaded={ !isLoading } fontWeight={ 400 } my={ 1 }>
......
...@@ -13,18 +13,32 @@ const BORDER_DEFAULT = 'none'; ...@@ -13,18 +13,32 @@ const BORDER_DEFAULT = 'none';
const HeroBanner = () => { const HeroBanner = () => {
const background = useColorModeValue( const background = useColorModeValue(
config.UI.homepage.heroBanner?.background?.[0] || config.UI.homepage.plate.background || BACKGROUND_DEFAULT, // light mode
config.UI.homepage.heroBanner?.background?.[1] || config.UI.homepage.plate.background || BACKGROUND_DEFAULT, config.UI.homepage.heroBanner?.background?.[0] ||
config.UI.homepage.plate.background ||
BACKGROUND_DEFAULT,
// dark mode
config.UI.homepage.heroBanner?.background?.[1] ||
config.UI.homepage.heroBanner?.background?.[0] ||
config.UI.homepage.plate.background ||
BACKGROUND_DEFAULT,
); );
const textColor = useColorModeValue( const textColor = useColorModeValue(
config.UI.homepage.heroBanner?.text_color?.[0] || config.UI.homepage.plate.textColor || TEXT_COLOR_DEFAULT, // light mode
config.UI.homepage.heroBanner?.text_color?.[1] || config.UI.homepage.plate.textColor || TEXT_COLOR_DEFAULT, config.UI.homepage.heroBanner?.text_color?.[0] ||
config.UI.homepage.plate.textColor ||
TEXT_COLOR_DEFAULT,
// dark mode
config.UI.homepage.heroBanner?.text_color?.[1] ||
config.UI.homepage.heroBanner?.text_color?.[0] ||
config.UI.homepage.plate.textColor ||
TEXT_COLOR_DEFAULT,
); );
const border = useColorModeValue( const border = useColorModeValue(
config.UI.homepage.heroBanner?.border?.[0] || BORDER_DEFAULT, config.UI.homepage.heroBanner?.border?.[0] || BORDER_DEFAULT,
config.UI.homepage.heroBanner?.border?.[1] || BORDER_DEFAULT, config.UI.homepage.heroBanner?.border?.[1] || config.UI.homepage.heroBanner?.border?.[0] || BORDER_DEFAULT,
); );
return ( return (
......
...@@ -24,6 +24,7 @@ import AddressBlocksValidated from 'ui/address/AddressBlocksValidated'; ...@@ -24,6 +24,7 @@ import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance'; import AddressCoinBalance from 'ui/address/AddressCoinBalance';
import AddressContract from 'ui/address/AddressContract'; import AddressContract from 'ui/address/AddressContract';
import AddressDetails from 'ui/address/AddressDetails'; import AddressDetails from 'ui/address/AddressDetails';
import AddressEpochRewards from 'ui/address/AddressEpochRewards';
import AddressInternalTxs from 'ui/address/AddressInternalTxs'; import AddressInternalTxs from 'ui/address/AddressInternalTxs';
import AddressLogs from 'ui/address/AddressLogs'; import AddressLogs from 'ui/address/AddressLogs';
import AddressMud from 'ui/address/AddressMud'; import AddressMud from 'ui/address/AddressMud';
...@@ -195,6 +196,12 @@ const AddressPageContent = () => { ...@@ -195,6 +196,12 @@ const AddressPageContent = () => {
count: addressTabsCountersQuery.data?.internal_txs_count, count: addressTabsCountersQuery.data?.internal_txs_count,
component: <AddressInternalTxs scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>, component: <AddressInternalTxs scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
}, },
addressTabsCountersQuery.data?.celo_election_rewards_count ? {
id: 'epoch_rewards',
title: 'Epoch rewards',
count: addressTabsCountersQuery.data?.celo_election_rewards_count,
component: <AddressEpochRewards scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
} : undefined,
{ {
id: 'coin_balance_history', id: 'coin_balance_history',
title: 'Coin balance history', title: 'Coin balance history',
...@@ -282,7 +289,7 @@ const AddressPageContent = () => { ...@@ -282,7 +289,7 @@ const AddressPageContent = () => {
{ slug: 'mud', name: 'MUD World', tagType: 'custom' as const, ordinal: -10 } : { slug: 'mud', name: 'MUD World', tagType: 'custom' as const, ordinal: -10 } :
undefined, undefined,
...formatUserTags(addressQuery.data), ...formatUserTags(addressQuery.data),
...(addressMetadataQuery.data?.addresses?.[hash.toLowerCase()]?.tags || []), ...(addressMetadataQuery.data?.addresses?.[hash.toLowerCase()]?.tags.filter(tag => tag.tagType !== 'note') || []),
].filter(Boolean).sort(sortEntityTags); ].filter(Boolean).sort(sortEntityTags);
}, [ addressMetadataQuery.data, addressQuery.data, hash, isSafeAddress, userOpsAccountQuery.data, mudTablesCountQuery.data, usernameApiTag ]); }, [ addressMetadataQuery.data, addressQuery.data, hash, isSafeAddress, userOpsAccountQuery.data, mudTablesCountQuery.data, usernameApiTag ]);
......
...@@ -110,7 +110,10 @@ const Chart = () => { ...@@ -110,7 +110,10 @@ const Chart = () => {
router.push({ router.push({
pathname: router.pathname, pathname: router.pathname,
query: { ...router.query, resolution }, query: { ...router.query, resolution },
}); },
undefined,
{ shallow: true },
);
}, [ setResolution, router ]); }, [ setResolution, router ]);
const handleReset = React.useCallback(() => { const handleReset = React.useCallback(() => {
......
...@@ -45,7 +45,7 @@ const EntityTags = ({ tags, className, isLoading }: Props) => { ...@@ -45,7 +45,7 @@ const EntityTags = ({ tags, className, isLoading }: Props) => {
+{ tags.length - visibleNum } +{ tags.length - visibleNum }
</Tag> </Tag>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent w="300px"> <PopoverContent maxW="300px" w="auto">
<PopoverBody > <PopoverBody >
<Flex columnGap={ 2 } rowGap={ 2 } flexWrap="wrap"> <Flex columnGap={ 2 } rowGap={ 2 } flexWrap="wrap">
{ tags.slice(visibleNum).map((tag) => <EntityTag key={ tag.slug } data={ tag }/>) } { tags.slice(visibleNum).map((tag) => <EntityTag key={ tag.slug } data={ tag }/>) }
......
import type { EntityTag } from './types'; import type { EntityTag } from './types';
import { route } from 'nextjs-routes'; // import { route } from 'nextjs-routes';
export function getTagLinkParams(data: EntityTag): { type: 'external' | 'internal'; href: string } | undefined { export function getTagLinkParams(data: EntityTag): { type: 'external' | 'internal'; href: string } | undefined {
if (data.meta?.warpcastHandle) { if (data.meta?.warpcastHandle) {
...@@ -17,10 +17,10 @@ export function getTagLinkParams(data: EntityTag): { type: 'external' | 'interna ...@@ -17,10 +17,10 @@ export function getTagLinkParams(data: EntityTag): { type: 'external' | 'interna
}; };
} }
if (data.tagType === 'generic' || data.tagType === 'protocol') { // if (data.tagType === 'generic' || data.tagType === 'protocol') {
return { // return {
type: 'internal', // type: 'internal',
href: route({ pathname: '/accounts/label/[slug]', query: { slug: data.slug, tagType: data.tagType, tagName: data.name } }), // href: route({ pathname: '/accounts/label/[slug]', query: { slug: data.slug, tagType: data.tagType, tagName: data.name } }),
}; // };
} // }
} }
import { Tooltip } from '@chakra-ui/react';
import React from 'react';
import type { EpochRewardsType } from 'types/api/block';
import Tag from 'ui/shared/chakra/Tag';
type Props = {
type: EpochRewardsType;
isLoading?: boolean;
};
const TYPE_TAGS: Record<EpochRewardsType, { text: string; label: string; color: string }> = {
group: {
text: 'Validator group rewards',
// eslint-disable-next-line max-len
label: 'Reward given to a validator group. The address being viewed is the group\'s address; the associated address is the validator\'s address on whose behalf the reward was paid.',
color: 'teal',
},
validator: {
text: 'Validator rewards',
label: 'Reward given to a validator. The address being viewed is the validator\'s address; the associated address is the validator group\'s address.',
color: 'purple',
},
delegated_payment: {
text: 'Delegated payments',
// eslint-disable-next-line max-len
label: 'Reward portion delegated by a validator to another address. The address being viewed is the beneficiary receiving the reward; the associated address is the validator who set the delegation.',
color: 'blue',
},
voter: {
text: 'Voting rewards',
label: 'Reward given to a voter. The address being viewed is the voter\'s address; the associated address is the group address.',
color: 'yellow',
},
};
const EpochRewardTypeTag = ({ type, isLoading }: Props) => {
const { text, label, color } = TYPE_TAGS[type];
return (
<Tooltip label={ label } maxW="322px" textAlign="center">
<Tag colorScheme={ color } isLoading={ isLoading }>
{ text }
</Tag>
</Tooltip>
);
};
export default React.memo(EpochRewardTypeTag);
...@@ -106,7 +106,7 @@ const SearchBar = ({ isHomepage }: Props) => { ...@@ -106,7 +106,7 @@ const SearchBar = ({ isHomepage }: Props) => {
React.useEffect(() => { React.useEffect(() => {
handleSearchTermChange(''); handleSearchTermChange('');
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ router.pathname ]); }, [ router.asPath?.split('?')?.[0] ]);
React.useEffect(() => { React.useEffect(() => {
const inputEl = inputRef.current; const inputEl = inputRef.current;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment