Celo: Block views customizations (#2185)
* show flag next to epoch block * block view customizations * add hint with link * display block epoch transfers * show epoch election reward types * show reward details * show base fee token from API * display current epoch on main page * mobile view * tests * add infinite scroll loading to reward details resource * update useApiQuery options * update screenshots * update layout of reward details on desktop * change hints of block epoch reward distribution
@@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="2.4" height="16" rx="1.2" fill="currentColor"/>
|
||||
<path d="M2.9 2.1h9.18a1.5 1.5 0 0 1 1.5 1.5v5.6a1.5 1.5 0 0 1-1.5 1.5H2.9V2.1Z" stroke="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.12 2.4H2.4v2.667h2.72V2.4Zm5.44 0H7.84v2.667h2.72V2.4ZM2.4 7.733h2.72V10.4H2.4V7.733Zm8.16 0H7.84V10.4h2.72V7.733ZM5.12 5.067h2.72v2.666H5.12V5.067Zm8.16 0h-2.72v2.666h2.72V5.067Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 507 B |
@@ -51,7 +51,16 @@ import type {
|
||||
ArbitrumL2TxnBatchesItem,
|
||||
} from 'types/api/arbitrumL2';
|
||||
import type { TxBlobs, Blob } from 'types/api/blobs';
|
||||
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse, BlockCountdownResponse } from 'types/api/block';
|
||||
import type {
|
||||
BlocksResponse,
|
||||
BlockTransactionsResponse,
|
||||
Block,
|
||||
BlockFilters,
|
||||
BlockWithdrawalsResponse,
|
||||
BlockCountdownResponse,
|
||||
BlockEpoch,
|
||||
BlockEpochElectionRewardDetailsResponse,
|
||||
} from 'types/api/block';
|
||||
import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts';
|
||||
import type { BackendVersionConfig } from 'types/api/configs';
|
||||
import type {
|
||||
@@ -327,6 +336,16 @@ export const RESOURCES = {
|
||||
pathParams: [ 'height_or_hash' as const ],
|
||||
filterFields: [],
|
||||
},
|
||||
block_epoch: {
|
||||
path: '/api/v2/blocks/:height_or_hash/epoch',
|
||||
pathParams: [ 'height_or_hash' as const ],
|
||||
filterFields: [],
|
||||
},
|
||||
block_election_rewards: {
|
||||
path: '/api/v2/blocks/:height_or_hash/election-rewards/:reward_type',
|
||||
pathParams: [ 'height_or_hash' as const, 'reward_type' as const ],
|
||||
filterFields: [],
|
||||
},
|
||||
txs_stats: {
|
||||
path: '/api/v2/transactions/stats',
|
||||
},
|
||||
@@ -938,7 +957,7 @@ export interface ResourceError<T = unknown> {
|
||||
|
||||
export type ResourceErrorAccount<T> = ResourceError<{ errors: T }>
|
||||
|
||||
export type PaginatedResources = 'blocks' | 'block_txs' |
|
||||
export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_rewards' |
|
||||
'txs_validated' | 'txs_pending' | 'txs_with_blobs' | 'txs_watchlist' | 'txs_execution_node' |
|
||||
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' | 'tx_state_changes' | 'tx_blobs' |
|
||||
'addresses' |
|
||||
@@ -998,6 +1017,8 @@ Q extends 'block' ? Block :
|
||||
Q extends 'block_countdown' ? BlockCountdownResponse :
|
||||
Q extends 'block_txs' ? BlockTransactionsResponse :
|
||||
Q extends 'block_withdrawals' ? BlockWithdrawalsResponse :
|
||||
Q extends 'block_epoch' ? BlockEpoch :
|
||||
Q extends 'block_election_rewards' ? BlockEpochElectionRewardDetailsResponse :
|
||||
Q extends 'txs_stats' ? TransactionsStats :
|
||||
Q extends 'txs_validated' ? TransactionsResponseValidated :
|
||||
Q extends 'txs_pending' ? TransactionsResponsePending :
|
||||
|
||||
@@ -19,7 +19,7 @@ import type { ApiResource, ResourceName, ResourcePathParams } from './resources'
|
||||
|
||||
export interface Params<R extends ResourceName> {
|
||||
pathParams?: ResourcePathParams<R>;
|
||||
queryParams?: Record<string, string | Array<string> | number | boolean | undefined>;
|
||||
queryParams?: Record<string, string | Array<string> | number | boolean | undefined | null>;
|
||||
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal' | 'headers'>;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { InfiniteData, QueryKey, UseInfiniteQueryResult } from '@tanstack/react-query';
|
||||
import { useInfiniteQuery, type UseInfiniteQueryOptions } from '@tanstack/react-query';
|
||||
|
||||
import type { PaginatedResources, ResourceError, ResourcePayload } from 'lib/api/resources';
|
||||
import useApiFetch from 'lib/api/useApiFetch';
|
||||
import type { Params as ApiFetchParams } from 'lib/api/useApiFetch';
|
||||
|
||||
import { getResourceKey } from './useApiQuery';
|
||||
|
||||
type TQueryData<R extends PaginatedResources> = ResourcePayload<R>;
|
||||
type TError = ResourceError<unknown>;
|
||||
type TPageParam<R extends PaginatedResources> = ApiFetchParams<R>['queryParams'] | null;
|
||||
|
||||
export interface Params<R extends PaginatedResources> {
|
||||
resourceName: R;
|
||||
// eslint-disable-next-line max-len
|
||||
queryOptions?: Omit<UseInfiniteQueryOptions<TQueryData<R>, TError, InfiniteData<TQueryData<R>>, TQueryData<R>, QueryKey, TPageParam<R>>, 'queryKey' | 'queryFn' | 'getNextPageParam' | 'initialPageParam'>;
|
||||
pathParams?: ApiFetchParams<R>['pathParams'];
|
||||
}
|
||||
|
||||
type ReturnType<Resource extends PaginatedResources> = UseInfiniteQueryResult<InfiniteData<ResourcePayload<Resource>>, ResourceError<unknown>>;
|
||||
|
||||
export default function useApiInfiniteQuery<R extends PaginatedResources>({
|
||||
resourceName,
|
||||
queryOptions,
|
||||
pathParams,
|
||||
}: Params<R>): ReturnType<R> {
|
||||
const apiFetch = useApiFetch();
|
||||
|
||||
return useInfiniteQuery<TQueryData<R>, TError, InfiniteData<TQueryData<R>>, QueryKey, TPageParam<R>>({
|
||||
// eslint-disable-next-line @tanstack/query/exhaustive-deps
|
||||
queryKey: getResourceKey(resourceName, { pathParams }),
|
||||
queryFn: (context) => {
|
||||
const queryParams = 'pageParam' in context ? (context.pageParam || undefined) : undefined;
|
||||
return apiFetch(resourceName, { pathParams, queryParams }) as Promise<TQueryData<R>>;
|
||||
},
|
||||
initialPageParam: null,
|
||||
getNextPageParam: (lastPage) => {
|
||||
return lastPage.next_page_params as TPageParam<R>;
|
||||
},
|
||||
...queryOptions,
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { QueryKey, UseQueryOptions } from '@tanstack/react-query';
|
||||
import type { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { Params as FetchParams } from 'lib/hooks/useFetch';
|
||||
@@ -10,8 +10,7 @@ export interface Params<R extends ResourceName, E = unknown, D = ResourcePayload
|
||||
pathParams?: ResourcePathParams<R>;
|
||||
queryParams?: Record<string, string | Array<string> | number | boolean | undefined>;
|
||||
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'headers'>;
|
||||
queryOptions?: Omit<UseQueryOptions<ResourcePayload<R>, ResourceError<E>, D>, 'queryKey' | 'queryFn'>;
|
||||
queryKey?: QueryKey;
|
||||
queryOptions?: Partial<Omit<UseQueryOptions<ResourcePayload<R>, ResourceError<E>, D>, 'queryFn'>>;
|
||||
}
|
||||
|
||||
export function getResourceKey<R extends ResourceName>(resource: R, { pathParams, queryParams }: Params<R> = {}) {
|
||||
@@ -24,13 +23,13 @@ export function getResourceKey<R extends ResourceName>(resource: R, { pathParams
|
||||
|
||||
export default function useApiQuery<R extends ResourceName, E = unknown, D = ResourcePayload<R>>(
|
||||
resource: R,
|
||||
{ queryOptions, pathParams, queryParams, queryKey, fetchParams }: Params<R, E, D> = {},
|
||||
{ queryOptions, pathParams, queryParams, fetchParams }: Params<R, E, D> = {},
|
||||
) {
|
||||
const apiFetch = useApiFetch();
|
||||
|
||||
return useQuery<ResourcePayload<R>, ResourceError<E>, D>({
|
||||
// eslint-disable-next-line @tanstack/query/exhaustive-deps
|
||||
queryKey: queryKey || getResourceKey(resource, { pathParams, queryParams }),
|
||||
queryKey: queryOptions?.queryKey || getResourceKey(resource, { pathParams, queryParams }),
|
||||
queryFn: async({ signal }) => {
|
||||
// all errors and error typing is handled by react-query
|
||||
// so error response will never go to the data
|
||||
|
||||
@@ -3,6 +3,11 @@ import type { RpcBlock } from 'viem';
|
||||
|
||||
import type { Block, BlocksResponse } from 'types/api/block';
|
||||
|
||||
import { ZERO_ADDRESS } from 'lib/consts';
|
||||
|
||||
import * as addressMock from '../address/address';
|
||||
import * as tokenMock from '../tokens/tokenInfo';
|
||||
|
||||
export const base: Block = {
|
||||
base_fee_per_gas: '10000000000',
|
||||
burnt_fees: '5449200000000000',
|
||||
@@ -137,6 +142,34 @@ export const rootstock: Block = {
|
||||
minimum_gas_price: '59240000',
|
||||
};
|
||||
|
||||
export const celo: Block = {
|
||||
...base,
|
||||
celo: {
|
||||
base_fee: {
|
||||
token: tokenMock.tokenInfoERC20a,
|
||||
amount: '445690000000000',
|
||||
breakdown: [
|
||||
{
|
||||
address: addressMock.withName,
|
||||
amount: '356552000000000.0000000000000',
|
||||
percentage: 80,
|
||||
},
|
||||
{
|
||||
address: {
|
||||
...addressMock.withoutName,
|
||||
hash: ZERO_ADDRESS,
|
||||
},
|
||||
amount: '89138000000000.0000000000000',
|
||||
percentage: 20,
|
||||
},
|
||||
],
|
||||
recipient: addressMock.contract,
|
||||
},
|
||||
epoch_number: 1486,
|
||||
is_epoch_block: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const withBlobTxs: Block = {
|
||||
...base,
|
||||
blob_gas_price: '21518435987',
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import _padStart from 'lodash/padStart';
|
||||
|
||||
import type { BlockEpoch, BlockEpochElectionRewardDetails, BlockEpochElectionRewardDetailsResponse } from 'types/api/block';
|
||||
|
||||
import * as addressMock from '../address/address';
|
||||
import * as tokenMock from '../tokens/tokenInfo';
|
||||
import * as tokenTransferMock from '../tokens/tokenTransfer';
|
||||
|
||||
export const blockEpoch1: BlockEpoch = {
|
||||
number: 1486,
|
||||
distribution: {
|
||||
carbon_offsetting_transfer: tokenTransferMock.erc20,
|
||||
community_transfer: tokenTransferMock.erc20,
|
||||
reserve_bolster_transfer: null,
|
||||
},
|
||||
aggregated_election_rewards: {
|
||||
delegated_payment: {
|
||||
count: 0,
|
||||
total: '71210001063118670575',
|
||||
token: tokenMock.tokenInfoERC20d,
|
||||
},
|
||||
group: {
|
||||
count: 10,
|
||||
total: '157705500305820107521',
|
||||
token: tokenMock.tokenInfoERC20b,
|
||||
},
|
||||
validator: {
|
||||
count: 10,
|
||||
total: '1348139501689262297152',
|
||||
token: tokenMock.tokenInfoERC20c,
|
||||
},
|
||||
voter: {
|
||||
count: 38,
|
||||
total: '2244419545166303388',
|
||||
token: tokenMock.tokenInfoERC20a,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function getRewardDetailsItem(index: number): BlockEpochElectionRewardDetails {
|
||||
return {
|
||||
amount: `${ 100 - index }210001063118670575`,
|
||||
account: {
|
||||
...addressMock.withoutName,
|
||||
hash: `0x30D060F129817c4DE5fBc1366d53e19f43c8c6${ _padStart(String(index), 2, '0') }`,
|
||||
},
|
||||
associated_account: {
|
||||
...addressMock.withoutName,
|
||||
hash: `0x456f41406B32c45D59E539e4BBA3D7898c3584${ _padStart(String(index), 2, '0') }`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const electionRewardDetails1: BlockEpochElectionRewardDetailsResponse = {
|
||||
items: Array(15).fill('').map((item, index) => getRewardDetailsItem(index)),
|
||||
next_page_params: null,
|
||||
};
|
||||
@@ -29,6 +29,7 @@
|
||||
| "burger"
|
||||
| "certified"
|
||||
| "check"
|
||||
| "checkered_flag"
|
||||
| "clock-light"
|
||||
| "clock"
|
||||
| "coins/bitcoin"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Block } from 'types/api/block';
|
||||
import type { Block, BlockEpochElectionReward, BlockEpoch } from 'types/api/block';
|
||||
|
||||
import { ADDRESS_PARAMS } from './addressParams';
|
||||
import { TOKEN_INFO_ERC_20, TOKEN_TRANSFER_ERC_20 } from './token';
|
||||
|
||||
export const BLOCK_HASH = '0x8fa7b9e5e5e79deeb62d608db22ba9a5cb45388c7ebb9223ae77331c6080dc70';
|
||||
|
||||
@@ -35,3 +36,24 @@ export const BLOCK: Block = {
|
||||
type: 'block',
|
||||
uncles_hashes: [],
|
||||
};
|
||||
|
||||
const BLOCK_EPOCH_REWARD: BlockEpochElectionReward = {
|
||||
count: 10,
|
||||
total: '157705500305820107521',
|
||||
token: TOKEN_INFO_ERC_20,
|
||||
};
|
||||
|
||||
export const BLOCK_EPOCH: BlockEpoch = {
|
||||
number: 1486,
|
||||
aggregated_election_rewards: {
|
||||
group: BLOCK_EPOCH_REWARD,
|
||||
validator: BLOCK_EPOCH_REWARD,
|
||||
voter: BLOCK_EPOCH_REWARD,
|
||||
delegated_payment: BLOCK_EPOCH_REWARD,
|
||||
},
|
||||
distribution: {
|
||||
carbon_offsetting_transfer: TOKEN_TRANSFER_ERC_20,
|
||||
community_transfer: TOKEN_TRANSFER_ERC_20,
|
||||
reserve_bolster_transfer: TOKEN_TRANSFER_ERC_20,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,10 +11,11 @@ import type { TokenInstanceTransferPagination, TokenInstanceTransferResponse } f
|
||||
import type { TokenTransfer, TokenTransferPagination, TokenTransferResponse } from 'types/api/tokenTransfer';
|
||||
|
||||
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
|
||||
import { BLOCK_HASH } from './block';
|
||||
import { TX_HASH } from './tx';
|
||||
import { generateListStub } from './utils';
|
||||
|
||||
export const BLOCK_HASH = '0x8fa7b9e5e5e79deeb62d608db22ba9a5cb45388c7ebb9223ae77331c6080dc70';
|
||||
|
||||
export const TOKEN_INFO_ERC_20: TokenInfo<'ERC-20'> = {
|
||||
address: ADDRESS_HASH,
|
||||
circulating_market_cap: '117629601.61913824',
|
||||
|
||||
@@ -60,6 +60,7 @@ const colors = {
|
||||
facebook: '#4460A0',
|
||||
medium: '#231F20',
|
||||
reddit: '#FF4500',
|
||||
celo: '#FCFF52',
|
||||
};
|
||||
|
||||
export default colors;
|
||||
|
||||
@@ -3,10 +3,19 @@ import type { Reward } from 'types/api/reward';
|
||||
import type { Transaction } from 'types/api/transaction';
|
||||
|
||||
import type { ArbitrumBatchStatus, ArbitrumL2TxData } from './arbitrumL2';
|
||||
import type { TokenInfo } from './token';
|
||||
import type { TokenTransfer } from './tokenTransfer';
|
||||
import type { ZkSyncBatchesItem } from './zkSyncL2';
|
||||
|
||||
export type BlockType = 'block' | 'reorg' | 'uncle';
|
||||
|
||||
export interface BlockBaseFeeCelo {
|
||||
amount: string;
|
||||
breakdown: Array<{ amount: string; percentage: number; address: AddressParam }>;
|
||||
recipient: AddressParam;
|
||||
token: TokenInfo;
|
||||
}
|
||||
|
||||
export interface Block {
|
||||
height: number;
|
||||
timestamp: string;
|
||||
@@ -50,6 +59,12 @@ export interface Block {
|
||||
'batch_number': number | null;
|
||||
};
|
||||
arbitrum?: ArbitrumBlockData;
|
||||
// CELO FIELDS
|
||||
celo?: {
|
||||
epoch_number: number;
|
||||
is_epoch_block: boolean;
|
||||
base_fee?: BlockBaseFeeCelo;
|
||||
};
|
||||
}
|
||||
|
||||
type ArbitrumBlockData = {
|
||||
@@ -112,3 +127,35 @@ export interface BlockCountdownResponse {
|
||||
RemainingBlock: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface BlockEpochElectionReward {
|
||||
count: number;
|
||||
token: TokenInfo<'ERC-20'>;
|
||||
total: string;
|
||||
}
|
||||
|
||||
export interface BlockEpoch {
|
||||
number: number;
|
||||
distribution: {
|
||||
carbon_offsetting_transfer: TokenTransfer | null;
|
||||
community_transfer: TokenTransfer | null;
|
||||
reserve_bolster_transfer: TokenTransfer | null;
|
||||
};
|
||||
aggregated_election_rewards: {
|
||||
delegated_payment: BlockEpochElectionReward | null;
|
||||
group: BlockEpochElectionReward | null;
|
||||
validator: BlockEpochElectionReward | null;
|
||||
voter: BlockEpochElectionReward | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BlockEpochElectionRewardDetails {
|
||||
account: AddressParam;
|
||||
amount: string;
|
||||
associated_account: AddressParam;
|
||||
}
|
||||
|
||||
export interface BlockEpochElectionRewardDetailsResponse {
|
||||
items: Array<BlockEpochElectionRewardDetails>;
|
||||
next_page_params: null;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ export type HomeStats = {
|
||||
rootstock_locked_btc?: string | null;
|
||||
last_output_root_size?: string | null;
|
||||
secondary_coin_price?: string | null;
|
||||
celo?: {
|
||||
epoch_number: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type GasPrices = {
|
||||
|
||||
@@ -38,6 +38,7 @@ import Utilization from 'ui/shared/Utilization/Utilization';
|
||||
import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps';
|
||||
import ZkSyncL2TxnBatchHashesInfo from 'ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchHashesInfo';
|
||||
|
||||
import BlockDetailsBaseFeeCelo from './details/BlockDetailsBaseFeeCelo';
|
||||
import BlockDetailsBlobInfo from './details/BlockDetailsBlobInfo';
|
||||
import type { BlockQuery } from './useBlockQuery';
|
||||
|
||||
@@ -394,6 +395,8 @@ const BlockDetails = ({ query }: Props) => {
|
||||
|
||||
<DetailsInfoItemDivider/>
|
||||
|
||||
{ data.celo?.base_fee && <BlockDetailsBaseFeeCelo data={ data.celo.base_fee }/> }
|
||||
|
||||
<DetailsInfoItem.Label
|
||||
hint="The total gas amount used in the block and its percentage of gas filled in the block"
|
||||
isLoading={ isPlaceholderData }
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import { BLOCK_EPOCH } from 'stubs/block';
|
||||
import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
|
||||
import BlockEpochElectionRewards from './epochRewards/BlockEpochElectionRewards';
|
||||
import BlockEpochRewardsDistribution from './epochRewards/BlockEpochRewardsDistribution';
|
||||
|
||||
interface Props {
|
||||
heightOrHash: string;
|
||||
}
|
||||
|
||||
const BlockEpochRewards = ({ heightOrHash }: Props) => {
|
||||
const query = useApiQuery('block_epoch', {
|
||||
pathParams: {
|
||||
height_or_hash: heightOrHash,
|
||||
},
|
||||
queryOptions: {
|
||||
placeholderData: BLOCK_EPOCH,
|
||||
},
|
||||
});
|
||||
|
||||
if (query.isError) {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
if (!query.data) {
|
||||
return <span>No block epoch rewards data</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BlockEpochRewardsDistribution data={ query.data } isLoading={ query.isPlaceholderData }/>
|
||||
<BlockEpochElectionRewards data={ query.data } isLoading={ query.isPlaceholderData }/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(BlockEpochRewards);
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Grid } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { BlockBaseFeeCelo } from 'types/api/block';
|
||||
|
||||
import * as blockMock from 'mocks/blocks/block';
|
||||
import { test, expect } from 'playwright/lib';
|
||||
|
||||
import BlockDetailsBaseFeeCelo from './BlockDetailsBaseFeeCelo';
|
||||
|
||||
test('base view +@mobile', async({ render }) => {
|
||||
const component = await render(
|
||||
<Grid
|
||||
columnGap={ 8 }
|
||||
rowGap={{ base: 3, lg: 3 }}
|
||||
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(min-content, 200px) minmax(0, 1fr)' }}
|
||||
overflow="hidden"
|
||||
>
|
||||
<BlockDetailsBaseFeeCelo data={ blockMock.celo.celo?.base_fee as BlockBaseFeeCelo }/>
|
||||
</Grid>,
|
||||
);
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Box, Flex, Link } from '@chakra-ui/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressParam } from 'types/api/addressParams';
|
||||
import type { BlockBaseFeeCelo } from 'types/api/block';
|
||||
import type { TokenInfo } from 'types/api/token';
|
||||
|
||||
import { WEI, ZERO_ADDRESS } from 'lib/consts';
|
||||
import AddressFromTo from 'ui/shared/address/AddressFromTo';
|
||||
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
|
||||
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
type ItemProps = BlockBaseFeeCelo['breakdown'][number] & {
|
||||
addressFrom: AddressParam;
|
||||
token: TokenInfo;
|
||||
}
|
||||
|
||||
const BreakDownItem = ({ amount, percentage, address, addressFrom, token }: ItemProps) => {
|
||||
const isBurning = address.hash === ZERO_ADDRESS;
|
||||
|
||||
return (
|
||||
<Flex alignItems="center" columnGap={ 2 } rowGap={ 1 } flexWrap="wrap">
|
||||
<Box color="text_secondary">{ percentage }% of amount</Box>
|
||||
<Flex columnGap={ 2 }>
|
||||
{ BigNumber(amount).dividedBy(WEI).toFixed() }
|
||||
<TokenEntity token={ token } noCopy onlySymbol/>
|
||||
</Flex>
|
||||
{ isBurning ? (
|
||||
<>
|
||||
<AddressEntity address={ addressFrom } truncation="constant"/>
|
||||
<IconSvg name="flame" boxSize={ 5 } color="gray.500"/>
|
||||
<Box color="text_secondary">burnt</Box>
|
||||
</>
|
||||
) : <AddressFromTo from={ addressFrom } to={ address }/> }
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
data: BlockBaseFeeCelo;
|
||||
}
|
||||
|
||||
const BlockDetailsBaseFeeCelo = ({ data }: Props) => {
|
||||
const totalBaseFee = BigNumber(data.amount).dividedBy(WEI).toFixed();
|
||||
|
||||
const totalFeeLabel = (
|
||||
<Box whiteSpace="pre-wrap">
|
||||
<span>The FeeHandler regularly burns 80% of its tokens. Non-CELO tokens are swapped to CELO beforehand. The remaining 20% are sent to the </span>
|
||||
<Link isExternal href="https://www.ultragreen.money">Green Fund</Link>
|
||||
<span>.</span>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DetailsInfoItem.Label
|
||||
hint="The contract receiving the base fee, responsible for handling fee usage. This contract is controlled by governance process."
|
||||
>
|
||||
Base fee handler
|
||||
</DetailsInfoItem.Label>
|
||||
<DetailsInfoItem.Value>
|
||||
<AddressEntity address={ data.recipient }/>
|
||||
</DetailsInfoItem.Value>
|
||||
<DetailsInfoItem.Label
|
||||
hint={ totalFeeLabel }
|
||||
type="popover"
|
||||
>
|
||||
Base fee total
|
||||
</DetailsInfoItem.Label>
|
||||
<DetailsInfoItem.Value display="block">
|
||||
<Flex columnGap={ 2 }>
|
||||
{ totalBaseFee }
|
||||
<TokenEntity token={ data.token } noCopy onlySymbol/>
|
||||
</Flex>
|
||||
{ data.breakdown.length > 0 && (
|
||||
<Flex flexDir="column" rowGap={ 2 } mt={ 2 }>
|
||||
{ data.breakdown.map((item, index) => (
|
||||
<BreakDownItem
|
||||
key={ index }
|
||||
{ ...item }
|
||||
addressFrom={ data.recipient }
|
||||
token={ data.token }
|
||||
/>
|
||||
)) }
|
||||
</Flex>
|
||||
) }
|
||||
</DetailsInfoItem.Value>
|
||||
<DetailsInfoItemDivider/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(BlockDetailsBaseFeeCelo);
|
||||
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,102 @@
|
||||
import { Box, Grid, GridItem, Text, useColorModeValue } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { BlockEpoch } from 'types/api/block';
|
||||
import type { TokenInfo } from 'types/api/token';
|
||||
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import ContentLoader from 'ui/shared/ContentLoader';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import useLazyLoadedList from 'ui/shared/pagination/useLazyLoadedList';
|
||||
|
||||
import { formatRewardType, getRewardDetailsTableTitles } from './utils';
|
||||
|
||||
interface Props {
|
||||
type: keyof BlockEpoch['aggregated_election_rewards'];
|
||||
token: TokenInfo;
|
||||
}
|
||||
|
||||
const BlockEpochElectionRewardDetailsDesktop = ({ type, token }: Props) => {
|
||||
const rootRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const heightOrHash = getQueryParamString(router.query.height_or_hash);
|
||||
|
||||
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
|
||||
|
||||
const { cutRef, query } = useLazyLoadedList({
|
||||
rootRef,
|
||||
resourceName: 'block_election_rewards',
|
||||
pathParams: { height_or_hash: heightOrHash, reward_type: formatRewardType(type) },
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
},
|
||||
});
|
||||
|
||||
const titles = getRewardDetailsTableTitles(type);
|
||||
|
||||
return (
|
||||
<Box
|
||||
p={ 4 }
|
||||
bgColor={ bgColor }
|
||||
borderRadius="base"
|
||||
maxH="360px"
|
||||
overflowY="scroll"
|
||||
fontWeight={ 400 }
|
||||
lineHeight={ 5 }
|
||||
>
|
||||
{ query.data && (
|
||||
<Grid
|
||||
gridTemplateColumns="min-content min-content min-content"
|
||||
rowGap={ 5 }
|
||||
columnGap={ 5 }
|
||||
>
|
||||
<GridItem fontWeight={ 600 } mb={ 1 } whiteSpace="nowrap">
|
||||
{ titles[0] }
|
||||
</GridItem>
|
||||
<GridItem fontWeight={ 600 } mb={ 1 } whiteSpace="nowrap" textAlign="right">
|
||||
Amount { token.symbol }
|
||||
</GridItem>
|
||||
<GridItem fontWeight={ 600 } mb={ 1 } whiteSpace="nowrap">
|
||||
{ titles[1] }
|
||||
</GridItem>
|
||||
|
||||
{ query.data?.pages
|
||||
.map((page) => page.items)
|
||||
.flat()
|
||||
.map((item, index) => {
|
||||
|
||||
const amount = getCurrencyValue({
|
||||
value: item.amount,
|
||||
decimals: token.decimals,
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment key={ index }>
|
||||
<GridItem>
|
||||
<AddressEntity address={ item.account } noIcon truncation="constant"/>
|
||||
</GridItem>
|
||||
<GridItem textAlign="right">
|
||||
{ amount.valueStr }
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<AddressEntity address={ item.associated_account } noIcon truncation="constant"/>
|
||||
</GridItem>
|
||||
</React.Fragment>
|
||||
);
|
||||
}) }
|
||||
</Grid>
|
||||
) }
|
||||
|
||||
{ query.isFetching && <ContentLoader maxW="200px" mt={ 3 }/> }
|
||||
|
||||
{ query.isError && <Text color="error" mt={ 3 }>Something went wrong. Unable to load next page.</Text> }
|
||||
|
||||
<Box h="0" w="100px" ref={ cutRef }/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(BlockEpochElectionRewardDetailsDesktop);
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Box, Flex, Text, useColorModeValue } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { BlockEpoch } from 'types/api/block';
|
||||
import type { TokenInfo } from 'types/api/token';
|
||||
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import ContentLoader from 'ui/shared/ContentLoader';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
import useLazyLoadedList from 'ui/shared/pagination/useLazyLoadedList';
|
||||
|
||||
import { formatRewardType } from './utils';
|
||||
|
||||
interface Props {
|
||||
type: keyof BlockEpoch['aggregated_election_rewards'];
|
||||
token: TokenInfo;
|
||||
}
|
||||
|
||||
const BlockEpochElectionRewardDetailsMobile = ({ type, token }: Props) => {
|
||||
const rootRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const heightOrHash = getQueryParamString(router.query.height_or_hash);
|
||||
|
||||
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
|
||||
|
||||
const { cutRef, query } = useLazyLoadedList({
|
||||
rootRef,
|
||||
resourceName: 'block_election_rewards',
|
||||
pathParams: { height_or_hash: heightOrHash, reward_type: formatRewardType(type) },
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" rowGap={ 3 } p={ 4 } bgColor={ bgColor } borderRadius="base" maxH="360px" overflowY="scroll">
|
||||
|
||||
{ query.data?.pages
|
||||
.map((page) => page.items)
|
||||
.flat()
|
||||
.map((item, index) => {
|
||||
|
||||
const amount = getCurrencyValue({
|
||||
value: item.amount,
|
||||
decimals: token.decimals,
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex key={ index } flexDir="column" alignItems="flex-start" rowGap={ 1 } fontWeight={ 400 }>
|
||||
<AddressEntity address={ item.account } noIcon w="100%"/>
|
||||
<Flex columnGap={ 1 } alignItems="center">
|
||||
<Box flexShrink={ 0 } color="text_secondary">got</Box>
|
||||
<Box>{ amount.valueStr }</Box>
|
||||
<TokenEntity token={ token } noIcon onlySymbol w="auto"/>
|
||||
</Flex>
|
||||
<Flex columnGap={ 1 } alignItems="center" w="100%">
|
||||
<Box flexShrink={ 0 } color="text_secondary">on behalf of</Box>
|
||||
<AddressEntity address={ item.associated_account } noIcon/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}) }
|
||||
|
||||
{ query.isFetching && <ContentLoader maxW="200px" mt={ 3 }/> }
|
||||
|
||||
{ query.isError && <Text color="error" mt={ 3 }>Something went wrong. Unable to load next page.</Text> }
|
||||
|
||||
<Box h="0" w="100px" mt="-12px" ref={ cutRef }/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(BlockEpochElectionRewardDetailsMobile);
|
||||
@@ -0,0 +1,25 @@
|
||||
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);
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
import * as blockEpochMock from 'mocks/blocks/epoch';
|
||||
import { test, expect } from 'playwright/lib';
|
||||
|
||||
import BlockEpochElectionRewards from './BlockEpochElectionRewards';
|
||||
|
||||
const heightOrHash = '1234';
|
||||
const hooksConfig = {
|
||||
router: {
|
||||
query: { height_or_hash: heightOrHash },
|
||||
},
|
||||
};
|
||||
|
||||
test('base view +@mobile', async({ render, mockApiResponse }) => {
|
||||
await mockApiResponse(
|
||||
'block_election_rewards',
|
||||
blockEpochMock.electionRewardDetails1,
|
||||
{ pathParams: { height_or_hash: heightOrHash, reward_type: 'voter' } },
|
||||
);
|
||||
const component = await render(<BlockEpochElectionRewards data={ blockEpochMock.blockEpoch1 }/>, { hooksConfig });
|
||||
await component.getByText('Voting rewards').click();
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Box, Heading, Hide, Show, Table, Tbody, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { BlockEpoch } from 'types/api/block';
|
||||
|
||||
import BlockEpochElectionRewardsListItem from './BlockEpochElectionRewardsListItem';
|
||||
import BlockEpochElectionRewardsTableItem from './BlockEpochElectionRewardsTableItem';
|
||||
|
||||
interface Props {
|
||||
data: BlockEpoch;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const BlockEpochElectionRewards = ({ data, isLoading }: Props) => {
|
||||
return (
|
||||
<Box mt={ 8 }>
|
||||
<Heading as="h4" size="sm" mb={ 3 }>Election rewards</Heading>
|
||||
<Hide below="lg" ssr={ false }>
|
||||
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th width="24px"/>
|
||||
<Th width="180px">Reward type</Th>
|
||||
<Th/>
|
||||
<Th isNumeric>Value</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{ Object.entries(data.aggregated_election_rewards).map((entry) => {
|
||||
const key = entry[0] as keyof BlockEpoch['aggregated_election_rewards'];
|
||||
const value = entry[1];
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<BlockEpochElectionRewardsTableItem
|
||||
key={ key }
|
||||
type={ key }
|
||||
isLoading={ isLoading }
|
||||
data={ value }
|
||||
/>
|
||||
);
|
||||
}) }
|
||||
</Tbody>
|
||||
</Table>
|
||||
</Hide>
|
||||
<Show below="lg" ssr={ false }>
|
||||
{ Object.entries(data.aggregated_election_rewards).map((entry) => {
|
||||
const key = entry[0] as keyof BlockEpoch['aggregated_election_rewards'];
|
||||
const value = entry[1];
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<BlockEpochElectionRewardsListItem
|
||||
key={ key }
|
||||
type={ key }
|
||||
isLoading={ isLoading }
|
||||
data={ value }
|
||||
/>
|
||||
);
|
||||
}) }
|
||||
</Show>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(BlockEpochElectionRewards);
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Box, Flex, IconButton, Skeleton, useDisclosure } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { BlockEpoch, BlockEpochElectionReward } from 'types/api/block';
|
||||
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import BlockEpochElectionRewardDetailsMobile from './BlockEpochElectionRewardDetailsMobile';
|
||||
import BlockEpochElectionRewardType from './BlockEpochElectionRewardType';
|
||||
|
||||
interface Props {
|
||||
data: BlockEpochElectionReward;
|
||||
type: keyof BlockEpoch['aggregated_election_rewards'];
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const BlockEpochElectionRewardsListItem = ({ data, isLoading, type }: Props) => {
|
||||
const section = useDisclosure();
|
||||
|
||||
const { valueStr } = getCurrencyValue({
|
||||
value: data.total,
|
||||
decimals: data.token.decimals,
|
||||
accuracy: 2,
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
py={ 3 }
|
||||
borderBottomWidth="1px"
|
||||
borderColor="divider"
|
||||
fontSize="sm"
|
||||
onClick={ isLoading || !data.count ? undefined : section.onToggle }
|
||||
cursor={ isLoading || !data.count ? undefined : 'pointer' }
|
||||
>
|
||||
<Flex my="3px" columnGap={ 3 } alignItems="center" flexWrap="wrap" rowGap={ 1 }>
|
||||
{ data.count ? (
|
||||
<Skeleton isLoaded={ !isLoading } display="flex" borderRadius="sm">
|
||||
<IconButton
|
||||
aria-label={ section.isOpen ? 'Collapse section' : 'Expand section' }
|
||||
variant="link"
|
||||
boxSize={ 6 }
|
||||
flexShrink={ 0 }
|
||||
icon={ (
|
||||
<IconSvg
|
||||
name="arrows/east-mini"
|
||||
boxSize={ 6 }
|
||||
transform={ section.isOpen ? 'rotate(270deg)' : 'rotate(180deg)' }
|
||||
transitionDuration="faster"
|
||||
/>
|
||||
) }
|
||||
/>
|
||||
</Skeleton>
|
||||
) : <Box boxSize={ 6 }/> }
|
||||
<BlockEpochElectionRewardType type={ type } isLoading={ isLoading }/>
|
||||
<Skeleton isLoaded={ !isLoading }>{ data.count }</Skeleton>
|
||||
<Flex columnGap={ 2 } alignItems="center" ml="auto" fontWeight={ 500 }>
|
||||
<Skeleton isLoaded={ !isLoading }>{ valueStr }</Skeleton>
|
||||
<TokenEntity
|
||||
token={ data.token }
|
||||
noCopy
|
||||
onlySymbol
|
||||
w="auto"
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{ section.isOpen && (
|
||||
<Box mt={ 2 }>
|
||||
<BlockEpochElectionRewardDetailsMobile type={ type } token={ data.token }/>
|
||||
</Box>
|
||||
) }
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(BlockEpochElectionRewardsListItem);
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Flex, IconButton, Skeleton, Td, Tr, useDisclosure } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { BlockEpoch, BlockEpochElectionReward } from 'types/api/block';
|
||||
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import BlockEpochElectionRewardDetailsDesktop from './BlockEpochElectionRewardDetailsDesktop';
|
||||
import BlockEpochElectionRewardType from './BlockEpochElectionRewardType';
|
||||
import { getRewardNumText } from './utils';
|
||||
|
||||
interface Props {
|
||||
data: BlockEpochElectionReward;
|
||||
type: keyof BlockEpoch['aggregated_election_rewards'];
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const BlockEpochElectionRewardsTableItem = ({ isLoading, data, type }: Props) => {
|
||||
const section = useDisclosure();
|
||||
|
||||
const { valueStr } = getCurrencyValue({
|
||||
value: data.total,
|
||||
decimals: data.token.decimals,
|
||||
});
|
||||
|
||||
const mainRowBorderColor = section.isOpen ? 'transparent' : 'divider';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
onClick={ isLoading || !data.count ? undefined : section.onToggle }
|
||||
cursor={ isLoading || !data.count ? undefined : 'pointer' }
|
||||
>
|
||||
<Td borderColor={ mainRowBorderColor }>
|
||||
{ Boolean(data.count) && (
|
||||
<Skeleton isLoaded={ !isLoading } display="flex" borderRadius="sm">
|
||||
<IconButton
|
||||
aria-label={ section.isOpen ? 'Collapse section' : 'Expand section' }
|
||||
variant="link"
|
||||
boxSize={ 6 }
|
||||
flexShrink={ 0 }
|
||||
icon={ (
|
||||
<IconSvg
|
||||
name="arrows/east-mini"
|
||||
boxSize={ 6 }
|
||||
transform={ section.isOpen ? 'rotate(270deg)' : 'rotate(180deg)' }
|
||||
transitionDuration="faster"
|
||||
/>
|
||||
) }
|
||||
/>
|
||||
</Skeleton>
|
||||
) }
|
||||
</Td>
|
||||
<Td borderColor={ mainRowBorderColor }>
|
||||
<BlockEpochElectionRewardType type={ type } isLoading={ isLoading }/>
|
||||
</Td>
|
||||
<Td borderColor={ mainRowBorderColor }>
|
||||
<Skeleton isLoaded={ !isLoading } fontWeight={ 400 } my={ 1 }>
|
||||
{ getRewardNumText(type, data.count) }
|
||||
</Skeleton>
|
||||
</Td>
|
||||
<Td borderColor={ mainRowBorderColor }>
|
||||
<Flex columnGap={ 2 } alignItems="center" justifyContent="flex-end" my="2px">
|
||||
<Skeleton isLoaded={ !isLoading }>{ valueStr }</Skeleton>
|
||||
<TokenEntity
|
||||
token={ data.token }
|
||||
noCopy
|
||||
onlySymbol
|
||||
w="auto"
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
</Flex>
|
||||
</Td>
|
||||
</Tr>
|
||||
{ section.isOpen && (
|
||||
<Tr>
|
||||
<Td/>
|
||||
<Td colSpan={ 3 } pr={ 0 } pt={ 0 }>
|
||||
<BlockEpochElectionRewardDetailsDesktop type={ type } token={ data.token }/>
|
||||
</Td>
|
||||
</Tr>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(BlockEpochElectionRewardsTableItem);
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Grid } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { BlockEpoch } from 'types/api/block';
|
||||
|
||||
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
|
||||
import TokenTransferSnippet from 'ui/shared/TokenTransferSnippet/TokenTransferSnippet';
|
||||
|
||||
interface Props {
|
||||
data: BlockEpoch;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const BlockEpochRewardsDistribution = ({ data, isLoading }: Props) => {
|
||||
|
||||
if (!data.distribution.community_transfer && !data.distribution.carbon_offsetting_transfer && !data.distribution.reserve_bolster_transfer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid
|
||||
columnGap={ 8 }
|
||||
rowGap={{ base: 3, lg: 3 }}
|
||||
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(min-content, 200px) minmax(0, 1fr)' }}
|
||||
overflow="hidden"
|
||||
>
|
||||
{ data.distribution.community_transfer && (
|
||||
<>
|
||||
<DetailsInfoItem.Label
|
||||
hint="Funds allocation to support Celo projects and community initiatives"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
Community fund
|
||||
</DetailsInfoItem.Label>
|
||||
<DetailsInfoItem.Value>
|
||||
<TokenTransferSnippet data={ data.distribution.community_transfer } isLoading={ isLoading } noAddressIcons={ false }/>
|
||||
</DetailsInfoItem.Value>
|
||||
</>
|
||||
) }
|
||||
{ data.distribution.carbon_offsetting_transfer && (
|
||||
<>
|
||||
<DetailsInfoItem.Label
|
||||
hint="Funds allocation to support projects that make Celo carbon-negative"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
Carbon offset fund
|
||||
</DetailsInfoItem.Label>
|
||||
<DetailsInfoItem.Value>
|
||||
<TokenTransferSnippet data={ data.distribution.carbon_offsetting_transfer } isLoading={ isLoading } noAddressIcons={ false }/>
|
||||
</DetailsInfoItem.Value>
|
||||
</>
|
||||
) }
|
||||
{ data.distribution.reserve_bolster_transfer && (
|
||||
<>
|
||||
<DetailsInfoItem.Label
|
||||
hint="Funds allocation to strengthen Celo’s reserve for network stability and security"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
Reserve bolster
|
||||
</DetailsInfoItem.Label>
|
||||
<DetailsInfoItem.Value>
|
||||
<TokenTransferSnippet data={ data.distribution.reserve_bolster_transfer } isLoading={ isLoading } noAddressIcons={ false }/>
|
||||
</DetailsInfoItem.Value>
|
||||
</>
|
||||
) }
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(BlockEpochRewardsDistribution);
|
||||
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,44 @@
|
||||
import type { BlockEpoch } from 'types/api/block';
|
||||
|
||||
export function getRewardNumText(type: keyof BlockEpoch['aggregated_election_rewards'], num: number) {
|
||||
const postfix1 = num !== 1 ? 's' : '';
|
||||
const postfix2 = num !== 1 ? 'es' : '';
|
||||
|
||||
const text = (() => {
|
||||
switch (type) {
|
||||
case 'delegated_payment':
|
||||
return 'payment' + postfix1;
|
||||
case 'group':
|
||||
return 'group reward' + postfix1;
|
||||
case 'validator':
|
||||
return 'validator' + postfix1;
|
||||
case 'voter':
|
||||
return 'voting address' + postfix2;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
})();
|
||||
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `${ num } ${ text }`;
|
||||
}
|
||||
|
||||
export function getRewardDetailsTableTitles(type: keyof BlockEpoch['aggregated_election_rewards']): [string, string] {
|
||||
switch (type) {
|
||||
case 'delegated_payment':
|
||||
return [ 'Beneficiary', 'Validator' ];
|
||||
case 'group':
|
||||
return [ 'Validator group', 'Associated validator' ];
|
||||
case 'validator':
|
||||
return [ 'Validator', 'Validator group' ];
|
||||
case 'voter':
|
||||
return [ 'Voter', 'Validator group' ];
|
||||
}
|
||||
}
|
||||
|
||||
export function formatRewardType(type: keyof BlockEpoch['aggregated_election_rewards']) {
|
||||
return type.replaceAll('_', '-');
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Flex, Skeleton, Text, Box } from '@chakra-ui/react';
|
||||
import { Flex, Skeleton, Text, Box, Tooltip } from '@chakra-ui/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import React from 'react';
|
||||
@@ -45,6 +45,11 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
|
||||
noIcon
|
||||
fontWeight={ 600 }
|
||||
/>
|
||||
{ data.celo?.is_epoch_block && (
|
||||
<Tooltip label={ `Finalized epoch #${ data.celo.epoch_number }` }>
|
||||
<IconSvg name="checkered_flag" boxSize={ 5 } p="1px" isLoading={ isLoading } flexShrink={ 0 }/>
|
||||
</Tooltip>
|
||||
) }
|
||||
</Flex>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ data.timestamp }
|
||||
|
||||
@@ -43,7 +43,7 @@ const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum
|
||||
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }>
|
||||
<Thead top={ top }>
|
||||
<Tr>
|
||||
<Th width="125px">Block</Th>
|
||||
<Th width="150px">Block</Th>
|
||||
<Th width="120px">Size, bytes</Th>
|
||||
{ !config.UI.views.block.hiddenFields?.miner &&
|
||||
<Th width={ `${ VALIDATOR_COL_WEIGHT / widthBase * 100 }%` } minW="160px">{ capitalize(getNetworkValidatorTitle()) }</Th> }
|
||||
|
||||
@@ -44,6 +44,11 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
|
||||
>
|
||||
<Td fontSize="sm">
|
||||
<Flex columnGap={ 2 } alignItems="center" mb={ 2 }>
|
||||
{ data.celo?.is_epoch_block && (
|
||||
<Tooltip label={ `Finalized epoch #${ data.celo.epoch_number }` }>
|
||||
<IconSvg name="checkered_flag" boxSize={ 5 } p="1px" isLoading={ isLoading } flexShrink={ 0 }/>
|
||||
</Tooltip>
|
||||
) }
|
||||
<Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations">
|
||||
<BlockEntity
|
||||
isLoading={ isLoading }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Heading, Flex, Text, VStack, Skeleton } from '@chakra-ui/react';
|
||||
import { chakra, Box, Heading, Flex, Text, VStack, Skeleton } from '@chakra-ui/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import React from 'react';
|
||||
@@ -108,6 +108,12 @@ const LatestBlocks = () => {
|
||||
</Text>
|
||||
</Skeleton>
|
||||
) }
|
||||
{ statsQueryResult.data?.celo && (
|
||||
<Box whiteSpace="pre-wrap" fontSize="sm">
|
||||
<span>Current epoch: </span>
|
||||
<chakra.span fontWeight={ 700 }>#{ statsQueryResult.data.celo.epoch_number }</chakra.span>
|
||||
</Box>
|
||||
) }
|
||||
<Box mt={ 3 }>
|
||||
{ content }
|
||||
</Box>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Flex,
|
||||
Grid,
|
||||
Skeleton,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { motion } from 'framer-motion';
|
||||
import React from 'react';
|
||||
@@ -14,6 +15,7 @@ import getBlockTotalReward from 'lib/block/getBlockTotalReward';
|
||||
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
type Props = {
|
||||
@@ -46,6 +48,11 @@ const LatestBlocksItem = ({ block, isLoading }: Props) => {
|
||||
fontWeight={ 500 }
|
||||
mr="auto"
|
||||
/>
|
||||
{ block.celo?.is_epoch_block && (
|
||||
<Tooltip label={ `Finalized epoch #${ block.celo.epoch_number }` }>
|
||||
<IconSvg name="checkered_flag" boxSize={ 5 } p="1px" ml={ 2 } isLoading={ isLoading } flexShrink={ 0 }/>
|
||||
</Tooltip>
|
||||
) }
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ block.timestamp }
|
||||
enableIncrement={ !isLoading }
|
||||
|
||||
@@ -140,6 +140,12 @@ const Stats = () => {
|
||||
value: `${ BigNumber(data.rootstock_locked_btc).div(WEI).dp(0).toFormat() } RBTC`,
|
||||
isLoading,
|
||||
},
|
||||
data.celo && {
|
||||
icon: 'hourglass' as const,
|
||||
label: 'Current epoch',
|
||||
value: `#${ data.celo.epoch_number }`,
|
||||
isLoading,
|
||||
},
|
||||
].filter(Boolean);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { chakra, Skeleton } from '@chakra-ui/react';
|
||||
import { chakra, Skeleton, Tooltip } from '@chakra-ui/react';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
@@ -14,6 +14,7 @@ import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import getNetworkValidationActionText from 'lib/networks/getNetworkValidationActionText';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import BlockDetails from 'ui/block/BlockDetails';
|
||||
import BlockEpochRewards from 'ui/block/BlockEpochRewards';
|
||||
import BlockWithdrawals from 'ui/block/BlockWithdrawals';
|
||||
import useBlockBlobTxsQuery from 'ui/block/useBlockBlobTxsQuery';
|
||||
import useBlockQuery from 'ui/block/useBlockQuery';
|
||||
@@ -21,6 +22,7 @@ import useBlockTxsQuery from 'ui/block/useBlockTxsQuery';
|
||||
import useBlockWithdrawalsQuery from 'ui/block/useBlockWithdrawalsQuery';
|
||||
import TextAd from 'ui/shared/ad/TextAd';
|
||||
import ServiceDegradationWarning from 'ui/shared/alerts/ServiceDegradationWarning';
|
||||
import Tag from 'ui/shared/chakra/Tag';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import NetworkExplorers from 'ui/shared/NetworkExplorers';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
@@ -94,7 +96,12 @@ const BlockPageContent = () => {
|
||||
</>
|
||||
),
|
||||
} : null,
|
||||
].filter(Boolean)), [ blockBlobTxsQuery, blockQuery, blockTxsQuery, blockWithdrawalsQuery, hasPagination ]);
|
||||
blockQuery.data?.celo?.is_epoch_block ? {
|
||||
id: 'epoch_rewards',
|
||||
title: 'Epoch rewards',
|
||||
component: <BlockEpochRewards heightOrHash={ heightOrHash }/>,
|
||||
} : null,
|
||||
].filter(Boolean)), [ blockBlobTxsQuery, blockQuery, blockTxsQuery, blockWithdrawalsQuery, hasPagination, heightOrHash ]);
|
||||
|
||||
let pagination;
|
||||
if (tab === 'txs') {
|
||||
@@ -139,6 +146,25 @@ const BlockPageContent = () => {
|
||||
return `Block #${ blockQuery.data?.height }`;
|
||||
}
|
||||
})();
|
||||
const contentAfter = (() => {
|
||||
if (!blockQuery.data?.celo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!blockQuery.data.celo.is_epoch_block) {
|
||||
return (
|
||||
<Tooltip label="Displays the epoch this block belongs to before the epoch is finalized" maxW="280px" textAlign="center">
|
||||
<Tag>Epoch #{ blockQuery.data.celo.epoch_number }</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip label="Displays the epoch finalized by this block" maxW="280px" textAlign="center">
|
||||
<Tag bgColor="celo" color="blackAlpha.800">Finalized epoch #{ blockQuery.data.celo.epoch_number }</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
})();
|
||||
const titleSecondRow = (
|
||||
<>
|
||||
{ !config.UI.views.block.hiddenFields?.miner && (
|
||||
@@ -166,6 +192,7 @@ const BlockPageContent = () => {
|
||||
<PageTitle
|
||||
title={ title }
|
||||
backLink={ backLink }
|
||||
contentAfter={ contentAfter }
|
||||
secondRow={ titleSecondRow }
|
||||
isLoading={ blockQuery.isPlaceholderData }
|
||||
/>
|
||||
|
||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
|
||||
import * as ContainerWithScrollY from 'ui/shared/ContainerWithScrollY';
|
||||
import Hint from 'ui/shared/Hint';
|
||||
import HintPopover from 'ui/shared/HintPopover';
|
||||
|
||||
const LabelScrollText = () => (
|
||||
<Text fontWeight={ 500 } variant="secondary" fontSize="xs" className="note" align="right">
|
||||
@@ -11,15 +12,16 @@ const LabelScrollText = () => (
|
||||
);
|
||||
|
||||
interface LabelProps {
|
||||
hint?: string;
|
||||
hint?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
id?: string;
|
||||
hasScroll?: boolean;
|
||||
type?: 'tooltip' | 'popover';
|
||||
}
|
||||
|
||||
const Label = chakra(({ hint, children, isLoading, id, className, hasScroll }: LabelProps) => {
|
||||
const Label = chakra(({ hint, children, isLoading, id, className, hasScroll, type }: LabelProps) => {
|
||||
return (
|
||||
<GridItem
|
||||
id={ id }
|
||||
@@ -29,7 +31,9 @@ const Label = chakra(({ hint, children, isLoading, id, className, hasScroll }: L
|
||||
_notFirst={{ mt: { base: 3, lg: 0 } }}
|
||||
>
|
||||
<Flex columnGap={ 2 } alignItems="flex-start">
|
||||
{ hint && <Hint label={ hint } isLoading={ isLoading } my={{ lg: '2px' }}/> }
|
||||
{ hint && (type === 'popover' ?
|
||||
<HintPopover label={ hint } isLoading={ isLoading } my={{ lg: '2px' }}/> :
|
||||
<Hint label={ hint } isLoading={ isLoading } my={{ lg: '2px' }}/>) }
|
||||
<Skeleton isLoaded={ !isLoading } fontWeight={{ base: 700, lg: 500 }}>
|
||||
{ children }
|
||||
{ hasScroll && <LabelScrollText/> }
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import type {
|
||||
PopoverBodyProps,
|
||||
PopoverContentProps,
|
||||
PopoverProps } from '@chakra-ui/react';
|
||||
import {
|
||||
Skeleton,
|
||||
DarkMode,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Portal,
|
||||
chakra,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import Popover from 'ui/shared/chakra/Popover';
|
||||
|
||||
import IconSvg from './IconSvg';
|
||||
|
||||
interface Props {
|
||||
label: React.ReactNode;
|
||||
className?: string;
|
||||
isLoading?: boolean;
|
||||
popoverProps?: Partial<PopoverProps>;
|
||||
popoverContentProps?: Partial<PopoverContentProps>;
|
||||
popoverBodyProps?: Partial<PopoverBodyProps>;
|
||||
}
|
||||
|
||||
const HintPopover = ({ label, isLoading, className, popoverProps, popoverContentProps, popoverBodyProps }: Props) => {
|
||||
const bgColor = useColorModeValue('gray.700', 'gray.900');
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton className={ className } boxSize={ 5 } borderRadius="sm"/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover trigger="hover" isLazy placement="top" { ...popoverProps }>
|
||||
<PopoverTrigger>
|
||||
<IconSvg className={ className } name="info" boxSize={ 5 } color="icon_info" _hover={{ color: 'link_hovered' }} cursor="pointer"/>
|
||||
</PopoverTrigger>
|
||||
<Portal>
|
||||
<PopoverContent bgColor={ bgColor } maxW={{ base: 'calc(100vw - 8px)', lg: '320px' }} borderRadius="sm" { ...popoverContentProps }>
|
||||
<PopoverArrow bgColor={ bgColor }/>
|
||||
<PopoverBody color="white" fontSize="sm" lineHeight="20px" px={ 2 } py="2px" { ...popoverBodyProps }>
|
||||
<DarkMode>
|
||||
{ label }
|
||||
</DarkMode>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(chakra(HintPopover));
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { Flex, Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type {
|
||||
TokenTransfer as TTokenTransfer,
|
||||
TokenTransfer,
|
||||
Erc20TotalPayload,
|
||||
Erc721TotalPayload,
|
||||
Erc1155TotalPayload,
|
||||
@@ -10,27 +10,34 @@ import type {
|
||||
} from 'types/api/tokenTransfer';
|
||||
|
||||
import AddressFromTo from 'ui/shared/address/AddressFromTo';
|
||||
import NftTokenTransferSnippet from 'ui/tx/NftTokenTransferSnippet';
|
||||
|
||||
import FtTokenTransferSnippet from '../FtTokenTransferSnippet';
|
||||
import TokenTransferSnippetFiat from './TokenTransferSnippetFiat';
|
||||
import TokenTransferSnippetNft from './TokenTransferSnippetNft';
|
||||
|
||||
interface Props {
|
||||
data: TTokenTransfer;
|
||||
data: TokenTransfer;
|
||||
noAddressIcons?: boolean;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const TxDetailsTokenTransfer = ({ data }: Props) => {
|
||||
const TokenTransferSnippet = ({ data, isLoading, noAddressIcons = true }: Props) => {
|
||||
|
||||
const content = (() => {
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton w="250px" h={ 6 }/>;
|
||||
}
|
||||
|
||||
switch (data.token.type) {
|
||||
case 'ERC-20': {
|
||||
const total = data.total as Erc20TotalPayload;
|
||||
return <FtTokenTransferSnippet token={ data.token } value={ total.value } decimals={ total.decimals }/>;
|
||||
return <TokenTransferSnippetFiat token={ data.token } value={ total.value } decimals={ total.decimals }/>;
|
||||
}
|
||||
|
||||
case 'ERC-721': {
|
||||
const total = data.total as Erc721TotalPayload;
|
||||
return (
|
||||
<NftTokenTransferSnippet
|
||||
<TokenTransferSnippetNft
|
||||
token={ data.token }
|
||||
tokenId={ total.token_id }
|
||||
value="1"
|
||||
@@ -41,7 +48,7 @@ const TxDetailsTokenTransfer = ({ data }: Props) => {
|
||||
case 'ERC-1155': {
|
||||
const total = data.total as Erc1155TotalPayload;
|
||||
return (
|
||||
<NftTokenTransferSnippet
|
||||
<TokenTransferSnippetNft
|
||||
key={ total.token_id }
|
||||
token={ data.token }
|
||||
tokenId={ total.token_id }
|
||||
@@ -55,7 +62,7 @@ const TxDetailsTokenTransfer = ({ data }: Props) => {
|
||||
|
||||
if (total.token_id !== null) {
|
||||
return (
|
||||
<NftTokenTransferSnippet
|
||||
<TokenTransferSnippetNft
|
||||
token={ data.token }
|
||||
tokenId={ total.token_id }
|
||||
value="1"
|
||||
@@ -66,7 +73,7 @@ const TxDetailsTokenTransfer = ({ data }: Props) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <FtTokenTransferSnippet token={ data.token } value={ total.value } decimals={ total.decimals }/>;
|
||||
return <TokenTransferSnippetFiat token={ data.token } value={ total.value } decimals={ total.decimals }/>;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,12 +93,13 @@ const TxDetailsTokenTransfer = ({ data }: Props) => {
|
||||
from={ data.from }
|
||||
to={ data.to }
|
||||
truncation="constant"
|
||||
noIcon
|
||||
noIcon={ noAddressIcons }
|
||||
fontWeight="500"
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
{ content }
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(TxDetailsTokenTransfer);
|
||||
export default React.memo(TokenTransferSnippet);
|
||||
@@ -33,6 +33,7 @@ const AddressEntityContentProxy = (props: ContentProps) => {
|
||||
{ ...props }
|
||||
truncation={ nameTag || implementationName || props.address.name ? 'tail' : props.truncation }
|
||||
text={ nameTag || implementationName || props.address.name || props.address.hash }
|
||||
isTooltipDisabled
|
||||
/>
|
||||
</Box>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -113,9 +113,10 @@ const Icon = ({ isLoading, iconSize, noIcon, name, color, borderRadius }: IconBa
|
||||
export interface ContentBaseProps extends Pick<EntityBaseProps, 'className' | 'isLoading' | 'truncation' | 'tailLength'> {
|
||||
asProp?: As;
|
||||
text: string;
|
||||
isTooltipDisabled?: boolean;
|
||||
}
|
||||
|
||||
const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dynamic', tailLength }: ContentBaseProps) => {
|
||||
const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dynamic', tailLength, isTooltipDisabled }: ContentBaseProps) => {
|
||||
|
||||
const children = (() => {
|
||||
switch (truncation) {
|
||||
@@ -125,6 +126,7 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna
|
||||
hash={ text }
|
||||
as={ asProp }
|
||||
type="long"
|
||||
isTooltipDisabled={ isTooltipDisabled }
|
||||
/>
|
||||
);
|
||||
case 'constant':
|
||||
@@ -132,6 +134,7 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna
|
||||
<HashStringShorten
|
||||
hash={ text }
|
||||
as={ asProp }
|
||||
isTooltipDisabled={ isTooltipDisabled }
|
||||
/>
|
||||
);
|
||||
case 'dynamic':
|
||||
@@ -140,6 +143,7 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna
|
||||
hash={ text }
|
||||
as={ asProp }
|
||||
tailLength={ tailLength }
|
||||
isTooltipDisabled={ isTooltipDisabled }
|
||||
/>
|
||||
);
|
||||
case 'tail':
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { InfiniteData, UseInfiniteQueryResult } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
|
||||
import type { PaginatedResources, ResourceError, ResourcePayload } from 'lib/api/resources';
|
||||
import type { Params as ApiInfiniteQueryParams } from 'lib/api/useApiInfiniteQuery';
|
||||
import useApiInfiniteQuery from 'lib/api/useApiInfiniteQuery';
|
||||
|
||||
interface Params<Resource extends PaginatedResources> extends ApiInfiniteQueryParams<Resource> {
|
||||
rootRef: React.RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
interface ReturnType<Resource extends PaginatedResources> {
|
||||
cutRef: (node?: Element | null) => void;
|
||||
query: UseInfiniteQueryResult<InfiniteData<ResourcePayload<Resource>>, ResourceError<unknown>>;
|
||||
}
|
||||
|
||||
export default function useLazyLoadedList<Resource extends PaginatedResources>({
|
||||
rootRef,
|
||||
resourceName,
|
||||
queryOptions,
|
||||
pathParams,
|
||||
}: Params<Resource>): ReturnType<Resource> {
|
||||
const query = useApiInfiniteQuery({
|
||||
resourceName,
|
||||
pathParams,
|
||||
queryOptions,
|
||||
});
|
||||
|
||||
const { ref, inView } = useInView({
|
||||
root: rootRef.current,
|
||||
triggerOnce: false,
|
||||
skip: queryOptions?.enabled === false || query.isFetchingNextPage || !query.hasNextPage,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (inView) {
|
||||
query.fetchNextPage();
|
||||
}
|
||||
// should run only on inView state change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ inView ]);
|
||||
|
||||
return { cutRef: ref, query };
|
||||
}
|
||||
@@ -45,8 +45,8 @@ const Sol2UmlDiagram = ({ addressHash }: Props) => {
|
||||
sources: composeSources(contractQuery.data),
|
||||
},
|
||||
},
|
||||
queryKey: [ 'visualize_sol2uml', addressHash ],
|
||||
queryOptions: {
|
||||
queryKey: [ 'visualize_sol2uml', addressHash ],
|
||||
enabled: Boolean(contractQuery.data),
|
||||
refetchOnMount: false,
|
||||
},
|
||||
|
||||
@@ -8,8 +8,7 @@ import { route } from 'nextjs-routes';
|
||||
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import LinkInternal from 'ui/shared/links/LinkInternal';
|
||||
|
||||
import TxDetailsTokenTransfer from './TxDetailsTokenTransfer';
|
||||
import TokenTransferSnippet from 'ui/shared/TokenTransferSnippet/TokenTransferSnippet';
|
||||
|
||||
interface Props {
|
||||
data: Array<TokenTransfer>;
|
||||
@@ -54,7 +53,7 @@ const TxDetailsTokenTransfers = ({ data, txHash, isOverflow }: Props) => {
|
||||
w="100%"
|
||||
overflow="hidden"
|
||||
>
|
||||
{ items.map((item, index) => <TxDetailsTokenTransfer key={ index } data={ item }/>) }
|
||||
{ items.map((item, index) => <TokenTransferSnippet key={ index } data={ item }/>) }
|
||||
</Flex>
|
||||
</DetailsInfoItem.Value>
|
||||
</React.Fragment>
|
||||
|
||||