Celo: support L2 epochs (#2784)
* epochs page * epoch rewards distribution * epoch election rewards * address epoch rewards * block epoch tag * add some tests * adjust epoch details for L1 epochs * support non-finalized epochs * fixes after design review * add truncation to block numbers on mobile * update epochs table layout * update timestamp hint * fix bug with subheading of first epoch * fixes of mobile view * update screenshots
@@ -4,14 +4,12 @@ import { getEnvValue } from '../utils';
|
||||
|
||||
const title = 'Celo chain';
|
||||
|
||||
const config: Feature<{ L2UpgradeBlock: number | undefined; BLOCKS_PER_EPOCH: number }> = (() => {
|
||||
const config: Feature<{ }> = (() => {
|
||||
|
||||
if (getEnvValue('NEXT_PUBLIC_CELO_ENABLED') === 'true') {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
L2UpgradeBlock: getEnvValue('NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK') ? Number(getEnvValue('NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK')) : undefined,
|
||||
BLOCKS_PER_EPOCH: 17_280,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ NEXT_PUBLIC_API_BASE_PATH=/
|
||||
NEXT_PUBLIC_API_HOST=celo-alfajores.blockscout.com
|
||||
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
|
||||
NEXT_PUBLIC_CELO_ENABLED=true
|
||||
NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK=26369280
|
||||
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
|
||||
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/celo.json
|
||||
@@ -24,6 +23,7 @@ NEXT_PUBLIC_GAS_TRACKER_ENABLED=false
|
||||
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x9767ce30754afad2a3279b9df2d13257f467c3dad4e0e601271e66d16dfd1641
|
||||
NEXT_PUBLIC_HAS_USER_OPS=true
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
|
||||
NEXT_PUBLIC_HOMEPAGE_STATS=['total_blocks','average_block_time','total_txs','wallet_addresses','current_epoch']
|
||||
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(252, 255, 82, 1)'],'text_color':['rgba(0, 0, 0, 1)']}
|
||||
NEXT_PUBLIC_IS_TESTNET=true
|
||||
NEXT_PUBLIC_MARKETPLACE_ENABLED=false
|
||||
|
||||
@@ -448,16 +448,6 @@ const celoSchema = yup
|
||||
.object()
|
||||
.shape({
|
||||
NEXT_PUBLIC_CELO_ENABLED: yup.boolean(),
|
||||
NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK: yup
|
||||
.string()
|
||||
.when('NEXT_PUBLIC_CELO_ENABLED', {
|
||||
is: (value: boolean) => value,
|
||||
then: (schema) => schema.min(0).optional(),
|
||||
otherwise: (schema) => schema.max(
|
||||
-1,
|
||||
'NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK cannot not be used if NEXT_PUBLIC_CELO_ENABLED is not set to "true"',
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
const adButlerConfigSchema = yup
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
NEXT_PUBLIC_CELO_ENABLED=true
|
||||
NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK=420
|
||||
NEXT_PUBLIC_CELO_ENABLED=true
|
||||
@@ -12,3 +12,4 @@
|
||||
| NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` | v1.12.0 | v1.29.0 | Replaced by NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL |
|
||||
| NEXT_PUBLIC_SWAP_BUTTON_URL | `string` | Application ID in the marketplace or website URL | - | - | `uniswap` | v1.24.0 | v1.31.0 | Replaced by NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS |
|
||||
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | v1.0.x+ | v1.35.0 | Replaced by NEXT_PUBLIC_HOMEPAGE_STATS |
|
||||
| NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK | `number` | Indicates the block number when the Celo-type chain transitioned to L2. This is used to display links to the Epoch block page from a regular block page. | - | - | `26369280` | v1.37.0+ | v2.2.0 | Removed; configuration done on the API side |
|
||||
|
||||
@@ -771,7 +771,6 @@ For blockchains that use the Celo platform. _Note_, that once the Celo mainnet b
|
||||
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_CELO_ENABLED | `boolean` | Indicates that it is a Celo-based chain. | - | - | `true` | v1.37.0+ |
|
||||
| NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK | `number` | Indicates the block number when the Celo-type chain transitioned to L2. This is used to display links to the Epoch block page from a regular block page. | - | - | `26369280` | v1.37.0+ |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.164.859a9.3 9.3 0 0 1 8.3 2.457l.047.046c.136.134.268.272.395.415l.03.033c.207.234.403.478.587.733l.039.055.15.214c.087.131.17.266.25.401.15.252.29.51.415.773.012.024.022.049.033.073.084.18.161.362.233.546l.03.08a9.3 9.3 0 0 1 .607 3.297l-.012.46a9.301 9.301 0 0 1-2.395 5.781l-.317.334a9.3 9.3 0 0 1-6.114 2.712l-.461.012A9.3 9.3 0 0 1 5.105 17.9l-.29-.187a9.303 9.303 0 0 1-.819-.615l-.055-.049a9.33 9.33 0 0 1-.438-.397l-.098-.098a8.814 8.814 0 0 1-.351-.371l-.078-.088a9.32 9.32 0 0 1-.16-.188l-.062-.075a9.397 9.397 0 0 1-.294-.383A9.297 9.297 0 0 1 8.164.859Zm1.817 1.509a7.617 7.617 0 0 0-3.994 1.13l-.237.153A7.616 7.616 0 0 0 3.06 6.81l-.113.259a7.615 7.615 0 0 0-.433 4.398l.06.277a7.613 7.613 0 0 0 2.024 3.622l.203.196a7.615 7.615 0 0 0 3.695 1.888l.278.05a7.614 7.614 0 0 0 4.12-.484l.26-.112a7.615 7.615 0 0 0 3.159-2.692l.152-.237a7.616 7.616 0 0 0 1.13-3.993l-.008-.378a7.614 7.614 0 0 0-1.962-4.732l-.26-.274a7.614 7.614 0 0 0-5.005-2.22l-.378-.01Zm.083 1.138c.192.02.374.104.512.242l.056.062c.123.15.191.339.191.534v5.29l3.631 3.626c.079.078.142.17.185.274l.027.078c.024.08.037.162.037.245l-.005.084a.84.84 0 0 1-.032.162l-.027.078a.852.852 0 0 1-.129.213l-.057.062a.837.837 0 0 1-.199.146l-.075.034a.841.841 0 0 1-.566.029l-.079-.029a.842.842 0 0 1-.274-.18l-3.877-3.877a.853.853 0 0 1-.146-.2l-.036-.075a.846.846 0 0 1-.063-.323V4.344c0-.223.09-.438.247-.596l.061-.055a.844.844 0 0 1 .535-.192l.083.005Z" fill="currentColor"/>
|
||||
<path d="M8.164.859a9.3 9.3 0 0 1 8.3 2.457l.047.046c.136.134.268.272.395.415l.03.033c.207.234.403.478.587.733l.039.055.15.214c.087.131.17.266.25.401.15.252.29.51.415.773.012.024.022.049.033.073.084.18.161.362.233.546l.03.08a9.3 9.3 0 0 1 .607 3.297l-.012.46a9.301 9.301 0 0 1-2.395 5.781l-.317.334a9.3 9.3 0 0 1-6.114 2.712l-.461.012A9.3 9.3 0 0 1 5.105 17.9l-.29-.187a9.303 9.303 0 0 1-.819-.615l-.055-.049a9.33 9.33 0 0 1-.438-.397l-.098-.098a8.814 8.814 0 0 1-.351-.371l-.078-.088a9.32 9.32 0 0 1-.16-.188l-.062-.075a9.397 9.397 0 0 1-.294-.383A9.297 9.297 0 0 1 8.164.859Zm1.817 1.509a7.617 7.617 0 0 0-3.994 1.13l-.237.153A7.616 7.616 0 0 0 3.06 6.81l-.113.259a7.615 7.615 0 0 0-.433 4.398l.06.277a7.613 7.613 0 0 0 2.024 3.622l.203.196a7.615 7.615 0 0 0 3.695 1.888l.278.05a7.614 7.614 0 0 0 4.12-.484l.26-.112a7.615 7.615 0 0 0 3.159-2.692l.152-.237a7.616 7.616 0 0 0 1.13-3.993l-.008-.378a7.614 7.614 0 0 0-1.962-4.732l-.26-.274a7.614 7.614 0 0 0-5.005-2.22l-.378-.01Zm.083 1.138c.192.02.374.104.512.242l.056.062c.123.15.191.339.191.534v5.29l3.631 3.626a.83.83 0 0 1 .185.274l.027.078a.85.85 0 0 1 .037.245l-.005.084a.84.84 0 0 1-.032.162l-.027.078a.852.852 0 0 1-.129.213l-.057.062a.837.837 0 0 1-.199.146l-.075.034a.841.841 0 0 1-.566.029l-.079-.029a.842.842 0 0 1-.274-.18l-3.877-3.877a.853.853 0 0 1-.146-.2l-.036-.075a.846.846 0 0 1-.063-.323V4.344c0-.223.09-.438.247-.596l.061-.055a.844.844 0 0 1 .535-.192l.083.005Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,3 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.125 6.906a1.257 1.257 0 0 0 .5-1V3.125a1.25 1.25 0 0 0-1.25-1.25h-8.75a1.25 1.25 0 0 0-1.25 1.25v2.813a1.258 1.258 0 0 0 .5 1L6.289 8l2.672 2-4.086 3.063a1.258 1.258 0 0 0-.5 1v2.812a1.25 1.25 0 0 0 1.25 1.25h8.75a1.25 1.25 0 0 0 1.25-1.25v-2.781a1.257 1.257 0 0 0-.5-1L11.039 10l1.774-1.344 2.312-1.75Zm-9.5-3.781h8.75v2.781l-.453.344H6.039l-.414-.313V3.126Zm8.75 10.969v2.781h-8.75v-2.813l3.75-2.812v1.875a.625.625 0 1 0 1.25 0v-1.867l3.75 2.836Z" fill="currentColor"/>
|
||||
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.052 4.708c.415.037.82.204 1.141.493l.132.132c.293.323.47.746.47 1.206v3.43a1.785 1.785 0 0 1-.128.654l-.096.201a1.875 1.875 0 0 1-.58.635l-.003.002L16.796 15l5.192 3.54.014.01.145.11.013.012c.163.14.304.31.411.504l.096.201c.083.206.127.428.129.654v3.43l-.01.195a1.8 1.8 0 0 1-.46 1.012l-.133.132c-.32.29-.726.455-1.141.492l-.179.008H8.923c-.417 0-.833-.13-1.177-.384l-.142-.116a1.821 1.821 0 0 1-.567-.978l-.026-.166-.002-.016L7 23.478v-3.483l.001-.021.014-.188.002-.018.03-.166c.038-.162.098-.319.179-.464l.01-.016.098-.155.01-.015.101-.128c.104-.12.223-.225.352-.314l.016-.012L12.986 15l-5.174-3.498-.015-.012a1.889 1.889 0 0 1-.453-.442l-.118-.186a1.799 1.799 0 0 1-.21-.63L7 10.008V6.539c0-.526.233-1.004.604-1.338l.142-.116c.343-.254.76-.385 1.177-.385h11.95l.179.008ZM9.139 20.182v2.98h11.52v-2.941l-4.691-3.198v1.823a1.07 1.07 0 0 1-.96 1.063l-.11.006a1.07 1.07 0 0 1-1.069-1.07v-1.832l-4.69 3.169Zm-.121.082h.002l.006-.006-.008.006Zm5.877-6.557 2.176-1.483H12.7l2.196 1.483Zm-5.756-3.89.395.268H20.21l.45-.307v-2.94H9.138v2.98Z" fill="currentColor"/>
|
||||
<path d="M21.052 4.708c.415.037.82.204 1.141.493l.132.132c.293.323.47.746.47 1.206v3.43a1.785 1.785 0 0 1-.128.654l-.096.201a1.875 1.875 0 0 1-.58.635l-.003.002L16.796 15l5.192 3.54.014.01.145.11.013.012c.163.14.304.31.411.504l.096.201c.083.206.127.428.129.654v3.43l-.01.195a1.8 1.8 0 0 1-.46 1.012l-.133.132c-.32.29-.726.455-1.141.492l-.179.008H8.923c-.417 0-.833-.13-1.177-.384l-.142-.116a1.821 1.821 0 0 1-.567-.978l-.026-.166-.002-.016L7 23.478v-3.483l.001-.021.014-.188.002-.018.03-.166c.038-.162.098-.319.179-.464l.01-.016.098-.155.01-.015.101-.128c.104-.12.223-.225.352-.314l.016-.012L12.986 15l-5.174-3.498-.015-.012a1.889 1.889 0 0 1-.453-.442l-.118-.186a1.799 1.799 0 0 1-.21-.63L7 10.008V6.539c0-.526.233-1.004.604-1.338l.142-.116c.343-.254.76-.385 1.177-.385h11.95l.179.008ZM9.139 20.182v2.98h11.52v-2.941l-4.691-3.198v1.823a1.07 1.07 0 0 1-.96 1.063l-.11.006a1.07 1.07 0 0 1-1.069-1.07v-1.832l-4.69 3.169Zm-.121.082h.002l.006-.006-.008.006Zm5.877-6.557 2.176-1.483H12.7l2.196 1.483Zm-5.756-3.89.395.268H20.21l.45-.307v-2.94H9.138v2.98Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 567 B After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.125 6.906a1.257 1.257 0 0 0 .5-1V3.125a1.25 1.25 0 0 0-1.25-1.25h-8.75a1.25 1.25 0 0 0-1.25 1.25v2.813a1.258 1.258 0 0 0 .5 1L6.289 8l2.672 2-4.086 3.063a1.258 1.258 0 0 0-.5 1v2.812a1.25 1.25 0 0 0 1.25 1.25h8.75a1.25 1.25 0 0 0 1.25-1.25v-2.781a1.257 1.257 0 0 0-.5-1L11.039 10l1.774-1.344 2.312-1.75Zm-9.5-3.781h8.75v2.781l-.453.344H6.039l-.414-.313V3.126Zm8.75 10.969v2.781h-8.75v-2.813l3.75-2.812v1.875a.625.625 0 1 0 1.25 0v-1.867l3.75 2.836Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 567 B |
@@ -116,7 +116,7 @@ export const GENERAL_API_ADDRESS_RESOURCES = {
|
||||
paginated: true,
|
||||
},
|
||||
address_epoch_rewards: {
|
||||
path: '/api/v2/addresses/:hash/election-rewards',
|
||||
path: '/api/v2/addresses/:hash/celo/election-rewards',
|
||||
pathParams: [ 'hash' as const ],
|
||||
filterFields: [],
|
||||
paginated: true,
|
||||
|
||||
@@ -6,8 +6,6 @@ import type {
|
||||
BlockFilters,
|
||||
BlockWithdrawalsResponse,
|
||||
BlockCountdownResponse,
|
||||
BlockEpoch,
|
||||
BlockEpochElectionRewardDetailsResponse,
|
||||
BlockInternalTransactionsResponse,
|
||||
} from 'types/api/block';
|
||||
import type { TTxsWithBlobsFilters } from 'types/api/txsFilters';
|
||||
@@ -39,17 +37,6 @@ export const GENERAL_API_BLOCK_RESOURCES = {
|
||||
filterFields: [],
|
||||
paginated: true,
|
||||
},
|
||||
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: [],
|
||||
paginated: true,
|
||||
},
|
||||
} satisfies Record<string, ApiResource>;
|
||||
|
||||
export type GeneralApiBlockResourceName = `general:${ keyof typeof GENERAL_API_BLOCK_RESOURCES }`;
|
||||
@@ -62,8 +49,6 @@ R extends 'general:block_countdown' ? BlockCountdownResponse :
|
||||
R extends 'general:block_txs' ? BlockTransactionsResponse :
|
||||
R extends 'general:block_internal_txs' ? BlockInternalTransactionsResponse :
|
||||
R extends 'general:block_withdrawals' ? BlockWithdrawalsResponse :
|
||||
R extends 'general:block_epoch' ? BlockEpoch :
|
||||
R extends 'general:block_election_rewards' ? BlockEpochElectionRewardDetailsResponse :
|
||||
never;
|
||||
/* eslint-enable @stylistic/indent */
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ import type {
|
||||
import type { Blob } from 'types/api/blobs';
|
||||
import type { Block } from 'types/api/block';
|
||||
import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts';
|
||||
import type { BackendVersionConfig, CsvExportConfig } from 'types/api/configs';
|
||||
import type { BackendVersionConfig, CeloConfig, CsvExportConfig } from 'types/api/configs';
|
||||
import type { CeloEpochDetails, CeloEpochElectionRewardDetailsResponse, CeloEpochListResponse } from 'types/api/epochs';
|
||||
import type { IndexingStatus } from 'types/api/indexingStatus';
|
||||
import type { NovesAccountHistoryResponse, NovesDescribeTxsResponse, NovesResponseData } from 'types/api/noves';
|
||||
import type {
|
||||
@@ -181,6 +182,23 @@ export const GENERAL_API_MISC_RESOURCES = {
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
|
||||
// EPOCHS
|
||||
epochs_celo: {
|
||||
path: '/api/v2/celo/epochs',
|
||||
filterFields: [],
|
||||
paginated: true,
|
||||
},
|
||||
epoch_celo: {
|
||||
path: '/api/v2/celo/epochs/:number',
|
||||
pathParams: [ 'number' as const ],
|
||||
},
|
||||
epoch_celo_election_rewards: {
|
||||
path: '/api/v2/celo/epochs/:number/election-rewards/:reward_type',
|
||||
pathParams: [ 'number' as const, 'reward_type' as const ],
|
||||
filterFields: [],
|
||||
paginated: true,
|
||||
},
|
||||
|
||||
// ADVANCED FILTER
|
||||
advanced_filter: {
|
||||
path: '/api/v2/advanced-filters',
|
||||
@@ -224,6 +242,9 @@ export const GENERAL_API_MISC_RESOURCES = {
|
||||
config_csv_export: {
|
||||
path: '/api/v2/config/csv-export',
|
||||
},
|
||||
config_celo: {
|
||||
path: '/api/v2/config/celo',
|
||||
},
|
||||
|
||||
// CSV EXPORT
|
||||
csv_export_token_holders: {
|
||||
@@ -261,6 +282,7 @@ R extends 'general:search' ? SearchResult :
|
||||
R extends 'general:search_check_redirect' ? SearchRedirectResult :
|
||||
R extends 'general:config_backend_version' ? BackendVersionConfig :
|
||||
R extends 'general:config_csv_export' ? CsvExportConfig :
|
||||
R extends 'general:config_celo' ? CeloConfig :
|
||||
R extends 'general:blob' ? Blob :
|
||||
R extends 'general:validators_stability' ? ValidatorsStabilityResponse :
|
||||
R extends 'general:validators_stability_counters' ? ValidatorsStabilityCountersResponse :
|
||||
@@ -268,6 +290,9 @@ R extends 'general:validators_blackfort' ? ValidatorsBlackfortResponse :
|
||||
R extends 'general:validators_blackfort_counters' ? ValidatorsBlackfortCountersResponse :
|
||||
R extends 'general:validators_zilliqa' ? ValidatorsZilliqaResponse :
|
||||
R extends 'general:validator_zilliqa' ? ValidatorZilliqa :
|
||||
R extends 'general:epochs_celo' ? CeloEpochListResponse :
|
||||
R extends 'general:epoch_celo' ? CeloEpochDetails :
|
||||
R extends 'general:epoch_celo_election_rewards' ? CeloEpochElectionRewardDetailsResponse :
|
||||
R extends 'general:user_ops' ? UserOpsResponse :
|
||||
R extends 'general:user_op' ? UserOp :
|
||||
R extends 'general:user_ops_account' ? UserOpsAccount :
|
||||
|
||||
@@ -118,6 +118,12 @@ export default function useNavItems(): ReturnType {
|
||||
icon: 'MUD_menu',
|
||||
isActive: pathname === '/mud-worlds',
|
||||
} : null;
|
||||
const epochs = config.features.celo.isEnabled ? {
|
||||
text: 'Epochs',
|
||||
nextRoute: { pathname: '/epochs' as const },
|
||||
icon: 'hourglass',
|
||||
isActive: pathname.startsWith('/epochs'),
|
||||
} : null;
|
||||
|
||||
const rollupFeature = config.features.rollup;
|
||||
|
||||
@@ -196,6 +202,7 @@ export default function useNavItems(): ReturnType {
|
||||
internalTxs,
|
||||
userOps,
|
||||
blocks,
|
||||
epochs,
|
||||
topAccounts,
|
||||
validators,
|
||||
verifiedContracts,
|
||||
|
||||
@@ -54,6 +54,8 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
|
||||
'/name-domains/[name]': 'Regular page',
|
||||
'/validators': 'Root page',
|
||||
'/validators/[id]': 'Regular page',
|
||||
'/epochs': 'Root page',
|
||||
'/epochs/[number]': 'Regular page',
|
||||
'/gas-tracker': 'Root page',
|
||||
'/mud-worlds': 'Root page',
|
||||
'/token-transfers': 'Root page',
|
||||
|
||||
@@ -57,6 +57,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/name-domains/[name]': DEFAULT_TEMPLATE,
|
||||
'/validators': DEFAULT_TEMPLATE,
|
||||
'/validators/[id]': DEFAULT_TEMPLATE,
|
||||
'/epochs': DEFAULT_TEMPLATE,
|
||||
'/epochs/[number]': DEFAULT_TEMPLATE,
|
||||
'/gas-tracker': 'Explore real-time %network_title% gas fees with Blockscout\'s advanced gas fee tracker. Get accurate %network_gwei% estimates and track transaction costs live.',
|
||||
'/mud-worlds': DEFAULT_TEMPLATE,
|
||||
'/token-transfers': DEFAULT_TEMPLATE,
|
||||
|
||||
@@ -54,6 +54,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/name-domains/[name]': '%network_name% %name% domain details',
|
||||
'/validators': '%network_name% validators list',
|
||||
'/validators/[id]': '%network_name% validator %id% details',
|
||||
'/epochs': '%network_name% epochs',
|
||||
'/epochs/[number]': '%network_name% epoch %number% details',
|
||||
'/gas-tracker': 'Track %network_name% gas fees in %network_gwei%',
|
||||
'/mud-worlds': '%network_name% MUD worlds list',
|
||||
'/token-transfers': '%network_name% token transfers',
|
||||
|
||||
@@ -52,6 +52,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
|
||||
'/name-domains/[name]': 'Domain details',
|
||||
'/validators': 'Validators list',
|
||||
'/validators/[id]': 'Validator details',
|
||||
'/epochs': 'Epochs',
|
||||
'/epochs/[number]': 'Epoch details',
|
||||
'/gas-tracker': 'Gas tracker',
|
||||
'/mud-worlds': 'MUD worlds',
|
||||
'/token-transfers': 'Token transfers',
|
||||
|
||||
@@ -7,5 +7,7 @@ export default function shortenString(string: string | null, charNumber: number
|
||||
return string;
|
||||
}
|
||||
|
||||
return string.slice(0, charNumber - 4) + '...' + string.slice(-4);
|
||||
const tailLength = charNumber < 8 ? 2 : 4;
|
||||
|
||||
return string.slice(0, charNumber - tailLength) + '...' + string.slice(-tailLength);
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ export const epochRewards: AddressEpochRewardsResponse = {
|
||||
amount: '136609473658452408568',
|
||||
account: withName,
|
||||
associated_account: withName,
|
||||
block_hash: '0x',
|
||||
block_number: 26369280,
|
||||
block_timestamp: '2022-05-15T13:16:24Z',
|
||||
epoch_number: 1526,
|
||||
token: tokenInfo,
|
||||
@@ -22,8 +20,6 @@ export const epochRewards: AddressEpochRewardsResponse = {
|
||||
amount: '117205842355246195095',
|
||||
account: withoutName,
|
||||
associated_account: withoutName,
|
||||
block_hash: '0x',
|
||||
block_number: 26352000,
|
||||
block_timestamp: '2022-05-15T13:16:24Z',
|
||||
epoch_number: 1525,
|
||||
token: tokenInfo,
|
||||
@@ -33,8 +29,6 @@ export const epochRewards: AddressEpochRewardsResponse = {
|
||||
amount: '125659647325556554060',
|
||||
account: withEns,
|
||||
associated_account: withEns,
|
||||
block_hash: '0x',
|
||||
block_number: 26300160,
|
||||
block_timestamp: '2022-05-15T13:16:24Z',
|
||||
epoch_number: 1524,
|
||||
token: tokenInfo,
|
||||
@@ -43,7 +37,7 @@ export const epochRewards: AddressEpochRewardsResponse = {
|
||||
next_page_params: {
|
||||
amount: '71952055594478242556',
|
||||
associated_account_address_hash: '0x30d060f129817c4de5fbc1366d53e19f43c8c64f',
|
||||
block_number: 25954560,
|
||||
epoch_number: 25954560,
|
||||
items_count: 50,
|
||||
type: 'delegated_payment',
|
||||
},
|
||||
|
||||
@@ -168,7 +168,7 @@ export const celo: Block = {
|
||||
recipient: addressMock.contract,
|
||||
},
|
||||
epoch_number: 1486,
|
||||
is_epoch_block: true,
|
||||
l1_era_finalized_epoch_number: 1485,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { padStart } from 'es-toolkit/compat';
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
import { padStart } from 'es-toolkit/compat';
|
||||
|
||||
import type { CeloEpochDetails, CeloEpochElectionRewardDetails, CeloEpochElectionRewardDetailsResponse, CeloEpochListResponse } from 'types/api/epochs';
|
||||
|
||||
import * as addressMock from '../address/address';
|
||||
import * as tokenMock from '../tokens/tokenInfo';
|
||||
import * as tokenTransferMock from '../tokens/tokenTransfer';
|
||||
|
||||
export const epoch1: CeloEpochDetails = {
|
||||
number: 1739,
|
||||
is_finalized: true,
|
||||
type: 'L1',
|
||||
timestamp: '2022-06-10T01:27:52.000000Z',
|
||||
start_block_number: 48477132,
|
||||
start_processing_block_hash: '0x9dece1eb0e26a95fdf57d2f3a65a6f2e00ca0192e8e3dd157eca0cd323670fa1',
|
||||
start_processing_block_number: 48563546,
|
||||
end_processing_block_hash: '0x9dece1eb0e26a95fdf57d2f3a65a6f2e00ca0192e8e3dd157eca0cd323670fa2',
|
||||
end_processing_block_number: 48563552,
|
||||
end_block_number: 48563551,
|
||||
distribution: {
|
||||
carbon_offsetting_transfer: tokenTransferMock.erc20,
|
||||
community_transfer: tokenTransferMock.erc20,
|
||||
transfers_total: {
|
||||
token: tokenMock.tokenInfoERC20a,
|
||||
total: {
|
||||
value: '1000000000000000000',
|
||||
decimals: '18',
|
||||
},
|
||||
},
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const epochUnfinalized: CeloEpochDetails = {
|
||||
number: 1740,
|
||||
is_finalized: false,
|
||||
type: 'L2',
|
||||
timestamp: null,
|
||||
start_block_number: 48477132,
|
||||
start_processing_block_hash: null,
|
||||
start_processing_block_number: null,
|
||||
end_processing_block_hash: null,
|
||||
end_processing_block_number: null,
|
||||
end_block_number: null,
|
||||
distribution: null,
|
||||
aggregated_election_rewards: null,
|
||||
};
|
||||
|
||||
export const list: CeloEpochListResponse = {
|
||||
items: [
|
||||
{
|
||||
timestamp: '2022-11-10T01:27:52.000000Z',
|
||||
number: 1739,
|
||||
type: 'L2',
|
||||
is_finalized: false,
|
||||
start_block_number: 48477132,
|
||||
end_block_number: null,
|
||||
distribution: null,
|
||||
},
|
||||
{
|
||||
timestamp: '2022-06-09T01:27:32.000000Z',
|
||||
number: 1738,
|
||||
type: 'L1',
|
||||
is_finalized: true,
|
||||
end_block_number: 18477131,
|
||||
start_block_number: 18390714,
|
||||
distribution: {
|
||||
carbon_offsetting_transfer: {
|
||||
decimals: '18',
|
||||
value: '1723199576750509130678',
|
||||
},
|
||||
community_transfer: {
|
||||
decimals: '18',
|
||||
value: '68927983070020365227',
|
||||
},
|
||||
transfers_total: {
|
||||
decimals: '18',
|
||||
value: '1792127559820529495905',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
next_page_params: null,
|
||||
};
|
||||
|
||||
function getRewardDetailsItem(index: number): CeloEpochElectionRewardDetails {
|
||||
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: CeloEpochElectionRewardDetailsResponse = {
|
||||
items: Array(15).fill('').map((item, index) => getRewardDetailsItem(index)),
|
||||
next_page_params: null,
|
||||
};
|
||||
@@ -390,6 +390,16 @@ export const tac: GetServerSideProps<Props> = async(context) => {
|
||||
return base(context);
|
||||
};
|
||||
|
||||
export const celo: GetServerSideProps<Props> = async(context) => {
|
||||
if (!config.features.celo.isEnabled) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
return base(context);
|
||||
};
|
||||
|
||||
export const interopMessages: GetServerSideProps<Props> = async(context) => {
|
||||
const rollupFeature = config.features.rollup;
|
||||
if (!rollupFeature.isEnabled || !rollupFeature.interopEnabled) {
|
||||
|
||||
@@ -43,6 +43,8 @@ declare module "nextjs-routes" {
|
||||
| StaticRoute<"/csv-export">
|
||||
| StaticRoute<"/deposits">
|
||||
| StaticRoute<"/dispute-games">
|
||||
| DynamicRoute<"/epochs/[number]", { "number": string }>
|
||||
| StaticRoute<"/epochs">
|
||||
| StaticRoute<"/gas-tracker">
|
||||
| StaticRoute<"/graphiql">
|
||||
| StaticRoute<"/">
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { NextPage } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
import type { Props } from 'nextjs/getServerSideProps';
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
const Epoch = dynamic(() => import('ui/pages/Epoch'), { ssr: false });
|
||||
|
||||
const Page: NextPage<Props> = (props: Props) => {
|
||||
return (
|
||||
<PageNextJs pathname="/epochs/[number]" query={ props.query }>
|
||||
<Epoch/>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { celo as getServerSideProps } from 'nextjs/getServerSideProps';
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { NextPage } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
const Epochs = dynamic(() => import('ui/pages/Epochs'), { ssr: false });
|
||||
|
||||
const Page: NextPage = () => {
|
||||
return (
|
||||
<PageNextJs pathname="/epochs">
|
||||
<Epochs/>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { celo as getServerSideProps } from 'nextjs/getServerSideProps';
|
||||
@@ -109,4 +109,7 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
|
||||
[ 'NEXT_PUBLIC_TAC_OPERATION_LIFECYCLE_API_HOST', 'http://localhost:3100' ],
|
||||
[ 'NEXT_PUBLIC_TAC_TON_EXPLORER_URL', 'https://testnet.tonviewer.com' ],
|
||||
],
|
||||
celo: [
|
||||
[ 'NEXT_PUBLIC_CELO_ENABLED', 'true' ],
|
||||
],
|
||||
};
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
| "graphQL"
|
||||
| "heart_filled"
|
||||
| "heart_outline"
|
||||
| "hourglass_slim"
|
||||
| "hourglass"
|
||||
| "info_filled"
|
||||
| "info"
|
||||
|
||||
@@ -119,11 +119,9 @@ export const ADDRESS_MUD_TABLE_ITEM: AddressMudTableItem = {
|
||||
|
||||
export const EPOCH_REWARD_ITEM: AddressEpochRewardsItem = {
|
||||
amount: '136609473658452408568',
|
||||
block_number: 10355938,
|
||||
block_timestamp: '2022-05-15T13:16:24Z',
|
||||
type: 'voter',
|
||||
token: TOKEN_INFO_ERC_20,
|
||||
block_hash: '0x5956a847d8089e254e02e5111cad6992b99ceb9e5c2dc4343fd53002834c4dc6',
|
||||
account: ADDRESS_PARAMS,
|
||||
epoch_number: 1234,
|
||||
associated_account: ADDRESS_PARAMS,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Block, BlockEpochElectionReward, BlockEpoch } from 'types/api/block';
|
||||
import type { Block } 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';
|
||||
|
||||
@@ -37,24 +36,3 @@ 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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { CeloEpochListItem, CeloEpochDetails, CeloEpochElectionReward } from 'types/api/epochs';
|
||||
|
||||
import { BLOCK_HASH } from './block';
|
||||
import { TOKEN_INFO_ERC_20, TOKEN_TRANSFER_ERC_20, TOKEN_TRANSFER_ERC_20_TOTAL } from './token';
|
||||
|
||||
export const CELO_EPOCH_ITEM: CeloEpochListItem = {
|
||||
timestamp: '2025-06-10T01:27:52.000000Z',
|
||||
number: 1739,
|
||||
end_block_number: 48563551,
|
||||
start_block_number: 48477132,
|
||||
type: 'L1',
|
||||
is_finalized: true,
|
||||
distribution: {
|
||||
carbon_offsetting_transfer: TOKEN_TRANSFER_ERC_20_TOTAL,
|
||||
community_transfer: TOKEN_TRANSFER_ERC_20_TOTAL,
|
||||
transfers_total: TOKEN_TRANSFER_ERC_20_TOTAL,
|
||||
},
|
||||
};
|
||||
|
||||
const CELO_EPOCH_REWARD: CeloEpochElectionReward = {
|
||||
count: 10,
|
||||
total: '157705500305820107521',
|
||||
token: TOKEN_INFO_ERC_20,
|
||||
};
|
||||
|
||||
export const CELO_EPOCH: CeloEpochDetails = {
|
||||
timestamp: '2025-06-10T01:27:52.000000Z',
|
||||
number: 1739,
|
||||
start_block_number: 48477132,
|
||||
start_processing_block_hash: BLOCK_HASH,
|
||||
start_processing_block_number: 48563546,
|
||||
end_processing_block_hash: BLOCK_HASH,
|
||||
end_processing_block_number: 48563552,
|
||||
end_block_number: 48563551,
|
||||
type: 'L1',
|
||||
is_finalized: true,
|
||||
distribution: {
|
||||
carbon_offsetting_transfer: TOKEN_TRANSFER_ERC_20,
|
||||
community_transfer: TOKEN_TRANSFER_ERC_20,
|
||||
transfers_total: {
|
||||
token: TOKEN_INFO_ERC_20,
|
||||
total: TOKEN_TRANSFER_ERC_20_TOTAL,
|
||||
},
|
||||
},
|
||||
aggregated_election_rewards: {
|
||||
group: CELO_EPOCH_REWARD,
|
||||
validator: CELO_EPOCH_REWARD,
|
||||
voter: CELO_EPOCH_REWARD,
|
||||
delegated_payment: CELO_EPOCH_REWARD,
|
||||
},
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
TokenType,
|
||||
} from 'types/api/token';
|
||||
import type { TokenInstanceTransferPagination, TokenInstanceTransferResponse } from 'types/api/tokens';
|
||||
import type { TokenTransfer, TokenTransferPagination, TokenTransferResponse } from 'types/api/tokenTransfer';
|
||||
import type { Erc20TotalPayload, TokenTransfer, TokenTransferPagination, TokenTransferResponse } from 'types/api/tokenTransfer';
|
||||
|
||||
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
|
||||
import { TX_HASH } from './tx';
|
||||
@@ -89,6 +89,11 @@ export const getTokenInstanceHoldersStub = (type?: TokenType, pagination: TokenH
|
||||
}
|
||||
};
|
||||
|
||||
export const TOKEN_TRANSFER_ERC_20_TOTAL: Erc20TotalPayload = {
|
||||
decimals: '18',
|
||||
value: '9851351626684503',
|
||||
};
|
||||
|
||||
export const TOKEN_TRANSFER_ERC_20: TokenTransfer = {
|
||||
block_hash: BLOCK_HASH,
|
||||
block_number: '123456',
|
||||
@@ -98,10 +103,7 @@ export const TOKEN_TRANSFER_ERC_20: TokenTransfer = {
|
||||
timestamp: '2022-06-24T10:22:11.000000Z',
|
||||
to: ADDRESS_PARAMS,
|
||||
token: TOKEN_INFO_ERC_20,
|
||||
total: {
|
||||
decimals: '18',
|
||||
value: '9851351626684503',
|
||||
},
|
||||
total: TOKEN_TRANSFER_ERC_20_TOTAL,
|
||||
transaction_hash: TX_HASH,
|
||||
type: 'token_minting',
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Transaction } from 'types/api/transaction';
|
||||
|
||||
import type { UserTags, AddressImplementation, AddressParam, AddressFilecoinParams } from './addressParams';
|
||||
import type { Block, EpochRewardsType } from './block';
|
||||
import type { Block } from './block';
|
||||
import type { SmartContractProxyType } from './contract';
|
||||
import type { CeloEpochRewardsType } from './epochs';
|
||||
import type { InternalTransaction } from './internalTransaction';
|
||||
import type { MudWorldSchema, MudWorldTable } from './mudWorlds';
|
||||
import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token';
|
||||
@@ -260,18 +261,16 @@ export type AddressEpochRewardsResponse = {
|
||||
next_page_params: {
|
||||
amount: string;
|
||||
associated_account_address_hash: string;
|
||||
block_number: number;
|
||||
epoch_number: number;
|
||||
items_count: number;
|
||||
type: EpochRewardsType;
|
||||
type: CeloEpochRewardsType;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type AddressEpochRewardsItem = {
|
||||
type: EpochRewardsType;
|
||||
type: CeloEpochRewardsType;
|
||||
token: TokenInfo;
|
||||
amount: string;
|
||||
block_number: number;
|
||||
block_hash: string;
|
||||
block_timestamp: string;
|
||||
account: AddressParam;
|
||||
epoch_number: number;
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { ArbitrumBatchStatus, ArbitrumL2TxData } from './arbitrumL2';
|
||||
import type { InternalTransaction } from './internalTransaction';
|
||||
import type { OptimisticL2BatchDataContainer, OptimisticL2BlobTypeEip4844, OptimisticL2BlobTypeCelestia } from './optimisticL2';
|
||||
import type { TokenInfo } from './token';
|
||||
import type { TokenTransfer } from './tokenTransfer';
|
||||
import type { ZkSyncBatchesItem } from './zkSyncL2';
|
||||
|
||||
export type BlockType = 'block' | 'reorg' | 'uncle';
|
||||
@@ -66,7 +65,7 @@ export interface Block {
|
||||
// CELO FIELDS
|
||||
celo?: {
|
||||
epoch_number: number;
|
||||
is_epoch_block: boolean;
|
||||
l1_era_finalized_epoch_number: number | null;
|
||||
base_fee?: BlockBaseFeeCelo;
|
||||
};
|
||||
// ZILLIQA FIELDS
|
||||
@@ -167,32 +166,3 @@ export interface BlockCountdownResponse {
|
||||
RemainingBlock: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface BlockEpochElectionReward {
|
||||
count: number;
|
||||
token: TokenInfo<'ERC-20'>;
|
||||
total: string;
|
||||
}
|
||||
|
||||
export type EpochRewardsType = 'group' | 'validator' | 'delegated_payment' | 'voter';
|
||||
|
||||
export interface BlockEpoch {
|
||||
number: number;
|
||||
distribution: {
|
||||
carbon_offsetting_transfer: TokenTransfer | null;
|
||||
community_transfer: TokenTransfer | null;
|
||||
reserve_bolster_transfer: TokenTransfer | null;
|
||||
} | null;
|
||||
aggregated_election_rewards: Record<EpochRewardsType, BlockEpochElectionReward | null> | null;
|
||||
}
|
||||
|
||||
export interface BlockEpochElectionRewardDetails {
|
||||
account: AddressParam;
|
||||
amount: string;
|
||||
associated_account: AddressParam;
|
||||
}
|
||||
|
||||
export interface BlockEpochElectionRewardDetailsResponse {
|
||||
items: Array<BlockEpochElectionRewardDetails>;
|
||||
next_page_params: null;
|
||||
}
|
||||
|
||||
@@ -5,3 +5,7 @@ export interface BackendVersionConfig {
|
||||
export interface CsvExportConfig {
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface CeloConfig {
|
||||
l2_migration_block: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { AddressParam } from './addressParams';
|
||||
import type { TokenInfo } from './token';
|
||||
import type { Erc20TotalPayload, TokenTransfer } from './tokenTransfer';
|
||||
|
||||
export type CeloEpochType = 'L1' | 'L2';
|
||||
|
||||
export type CeloEpochListItem = {
|
||||
number: number;
|
||||
type: CeloEpochType;
|
||||
is_finalized: boolean;
|
||||
start_block_number: number;
|
||||
end_block_number: number | null;
|
||||
timestamp: string | null;
|
||||
distribution: {
|
||||
carbon_offsetting_transfer: Erc20TotalPayload | null;
|
||||
community_transfer: Erc20TotalPayload | null;
|
||||
transfers_total: Erc20TotalPayload | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type CeloEpochListResponse = {
|
||||
items: Array<CeloEpochListItem>;
|
||||
next_page_params: {
|
||||
items_count: number;
|
||||
number: number;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type CeloEpochDetails = {
|
||||
number: number;
|
||||
type: CeloEpochType;
|
||||
is_finalized: boolean;
|
||||
timestamp: string | null;
|
||||
start_block_number: number;
|
||||
start_processing_block_hash: string | null;
|
||||
start_processing_block_number: number | null;
|
||||
end_block_number: number | null;
|
||||
end_processing_block_hash: string | null;
|
||||
end_processing_block_number: number | null;
|
||||
distribution: {
|
||||
carbon_offsetting_transfer: TokenTransfer | null;
|
||||
community_transfer: TokenTransfer | null;
|
||||
transfers_total: {
|
||||
token: TokenInfo<'ERC-20'> | null;
|
||||
total: Erc20TotalPayload | null;
|
||||
} | null;
|
||||
} | null;
|
||||
aggregated_election_rewards: Record<CeloEpochRewardsType, CeloEpochElectionReward | null> | null;
|
||||
};
|
||||
|
||||
export interface CeloEpochElectionReward {
|
||||
count: number;
|
||||
token: TokenInfo<'ERC-20'>;
|
||||
total: string;
|
||||
}
|
||||
|
||||
export type CeloEpochRewardsType = 'group' | 'validator' | 'delegated_payment' | 'voter';
|
||||
|
||||
export interface CeloEpochElectionRewardDetails {
|
||||
account: AddressParam;
|
||||
amount: string;
|
||||
associated_account: AddressParam;
|
||||
}
|
||||
|
||||
export interface CeloEpochElectionRewardDetailsResponse {
|
||||
items: Array<CeloEpochElectionRewardDetails>;
|
||||
next_page_params: null;
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
import Pagination from 'ui/shared/pagination/Pagination';
|
||||
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
|
||||
|
||||
import AddressCsvExportLink from './AddressCsvExportLink';
|
||||
// import AddressCsvExportLink from './AddressCsvExportLink';
|
||||
import AddressEpochRewardsListItem from './epochRewards/AddressEpochRewardsListItem';
|
||||
|
||||
type Props = {
|
||||
@@ -38,7 +38,7 @@ const AddressEpochRewards = ({ shouldRender = true, isQueryEnabled = true }: Pro
|
||||
items_count: 50,
|
||||
type: 'voter',
|
||||
associated_account_address_hash: '1',
|
||||
block_number: 10355938,
|
||||
epoch_number: 10355938,
|
||||
} }),
|
||||
},
|
||||
});
|
||||
@@ -59,7 +59,7 @@ const AddressEpochRewards = ({ shouldRender = true, isQueryEnabled = true }: Pro
|
||||
<Box hideFrom="lg">
|
||||
{ rewardsQuery.data.items.map((item, index) => (
|
||||
<AddressEpochRewardsListItem
|
||||
key={ item.block_hash + item.type + item.account.hash + item.associated_account.hash + (rewardsQuery.isPlaceholderData ? String(index) : '') }
|
||||
key={ item.epoch_number + item.type + item.account.hash + item.associated_account.hash + (rewardsQuery.isPlaceholderData ? String(index) : '') }
|
||||
item={ item }
|
||||
isLoading={ rewardsQuery.isPlaceholderData }
|
||||
/>
|
||||
@@ -70,13 +70,17 @@ const AddressEpochRewards = ({ shouldRender = true, isQueryEnabled = true }: Pro
|
||||
|
||||
const actionBar = rewardsQuery.pagination.isVisible ? (
|
||||
<ActionBar mt={ -6 }>
|
||||
<AddressCsvExportLink
|
||||
{ /* <AddressCsvExportLink
|
||||
address={ hash }
|
||||
isLoading={ rewardsQuery.pagination.isLoading }
|
||||
params={{ type: 'epoch-rewards' }}
|
||||
ml={{ lg: 'auto' }}
|
||||
/> */ }
|
||||
<Pagination
|
||||
ml="auto"
|
||||
// ml={{ base: 0, lg: 8 }}
|
||||
{ ...rewardsQuery.pagination }
|
||||
/>
|
||||
<Pagination ml={{ base: 0, lg: 8 }} { ...rewardsQuery.pagination }/>
|
||||
</ActionBar>
|
||||
) : null;
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 60 KiB |
@@ -5,7 +5,7 @@ import type { AddressEpochRewardsItem } from 'types/api/address';
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import EpochEntity from 'ui/shared/entities/epoch/EpochEntity';
|
||||
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
|
||||
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
|
||||
@@ -21,20 +21,9 @@ const AddressEpochRewardsListItem = ({ item, isLoading }: Props) => {
|
||||
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>
|
||||
<Skeleton loading={ isLoading }>
|
||||
{ item.epoch_number }
|
||||
</Skeleton>
|
||||
<EpochEntity number={ String(item.epoch_number) } noIcon isLoading={ isLoading }/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
|
||||
|
||||
@@ -19,7 +19,7 @@ const AddressEpochRewardsTable = ({ items, isLoading, top }: Props) => {
|
||||
<TableHeaderSticky top={ top }>
|
||||
<TableRow>
|
||||
<TableColumnHeader>
|
||||
Block
|
||||
Epoch
|
||||
<TimeFormatToggle/>
|
||||
</TableColumnHeader>
|
||||
<TableColumnHeader>Reward type</TableColumnHeader>
|
||||
@@ -31,7 +31,7 @@ const AddressEpochRewardsTable = ({ items, isLoading, top }: Props) => {
|
||||
{ items.map((item, index) => {
|
||||
return (
|
||||
<AddressEpochRewardsTableItem
|
||||
key={ item.block_hash + item.type + item.account.hash + item.associated_account.hash + (isLoading ? String(index) : '') }
|
||||
key={ item.epoch_number + item.type + item.account.hash + item.associated_account.hash + (isLoading ? String(index) : '') }
|
||||
item={ item }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Flex, Text } from '@chakra-ui/react';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressEpochRewardsItem } from 'types/api/address';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
import { TableCell, TableRow } from 'toolkit/chakra/table';
|
||||
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 TimeWithTooltip from 'ui/shared/time/TimeWithTooltip';
|
||||
@@ -23,10 +25,12 @@ const AddressEpochRewardsTableItem = ({ item, isLoading }: Props) => {
|
||||
<TableRow>
|
||||
<TableCell verticalAlign="middle">
|
||||
<Flex alignItems="center" gap={ 3 }>
|
||||
<BlockEntity number={ item.block_number } isLoading={ isLoading } noIcon fontWeight={ 600 }/>
|
||||
<Skeleton loading={ isLoading }>
|
||||
<Text color="text.secondary" fontWeight={ 600 }>{ `Epoch # ${ item.epoch_number }` }</Text>
|
||||
</Skeleton>
|
||||
<Link
|
||||
href={ route({ pathname: '/epochs/[number]', query: { number: String(item.epoch_number) } }) }
|
||||
loading={ isLoading }
|
||||
>
|
||||
{ item.epoch_number }
|
||||
</Link>
|
||||
<TimeWithTooltip timestamp={ item.block_timestamp } isLoading={ isLoading } color="text.secondary" fontWeight={ 400 }/>
|
||||
</Flex>
|
||||
</TableCell>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
import { Tag } from 'toolkit/chakra/tag';
|
||||
import { Tooltip } from 'toolkit/chakra/tooltip';
|
||||
@@ -13,39 +13,44 @@ interface Props {
|
||||
blockQuery: BlockQuery;
|
||||
}
|
||||
|
||||
const BlockCeloEpochTagRegular = ({ blockQuery }: Props) => {
|
||||
if (!blockQuery.data?.celo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key="epoch-tag-before-finalized"
|
||||
content="Displays the epoch this block belongs to before the epoch is finalized"
|
||||
>
|
||||
<Link href={ route({ pathname: '/epochs/[number]', query: { number: String(blockQuery.data.celo.epoch_number) } }) }>
|
||||
<Tag variant="clickable">Epoch #{ blockQuery.data.celo.epoch_number }</Tag>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const BlockCeloEpochTag = ({ blockQuery }: Props) => {
|
||||
if (!blockQuery.data?.celo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!blockQuery.data.celo.is_epoch_block) {
|
||||
const celoConfig = config.features.celo;
|
||||
const epochBlockNumber = celoConfig.isEnabled && celoConfig.L2UpgradeBlock && blockQuery.data.height <= celoConfig.L2UpgradeBlock ?
|
||||
blockQuery.data.celo.epoch_number * celoConfig.BLOCKS_PER_EPOCH :
|
||||
undefined;
|
||||
const content = epochBlockNumber ? (
|
||||
<Link href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: String(epochBlockNumber) } }) }>
|
||||
<Tag variant="clickable">Epoch #{ blockQuery.data.celo.epoch_number }</Tag>
|
||||
</Link>
|
||||
) : <Tag>Epoch #{ blockQuery.data.celo.epoch_number }</Tag>;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key="epoch-tag-before-finalized"
|
||||
content="Displays the epoch this block belongs to before the epoch is finalized"
|
||||
>
|
||||
{ content }
|
||||
</Tooltip>
|
||||
);
|
||||
if (!blockQuery.data.celo.l1_era_finalized_epoch_number) {
|
||||
return <BlockCeloEpochTagRegular blockQuery={ blockQuery }/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key="epoch-tag"
|
||||
content="Displays the epoch finalized by this block"
|
||||
>
|
||||
<Tag bgColor="celo" color="blackAlpha.800" > Finalized epoch #{ blockQuery.data.celo.epoch_number } </Tag>
|
||||
</Tooltip>
|
||||
<HStack gap={ 2 }>
|
||||
<Tooltip
|
||||
key="epoch-tag"
|
||||
content="Displays the epoch finalized by this block"
|
||||
>
|
||||
<Link href={ route({ pathname: '/epochs/[number]', query: { number: String(blockQuery.data.celo.l1_era_finalized_epoch_number) } }) }>
|
||||
<Tag bgColor="celo" color="blackAlpha.800" variant="clickable"> Finalized epoch #{ blockQuery.data.celo.l1_era_finalized_epoch_number } </Tag>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<BlockCeloEpochTagRegular blockQuery={ blockQuery }/>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
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('general:block_epoch', {
|
||||
pathParams: {
|
||||
height_or_hash: heightOrHash,
|
||||
},
|
||||
queryOptions: {
|
||||
placeholderData: BLOCK_EPOCH,
|
||||
},
|
||||
});
|
||||
|
||||
if (query.isError) {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
if (!query.data || (!query.data.aggregated_election_rewards && !query.data.distribution)) {
|
||||
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);
|
||||
@@ -1,35 +0,0 @@
|
||||
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', async({ render, mockApiResponse }) => {
|
||||
await mockApiResponse(
|
||||
'general:block_election_rewards',
|
||||
blockEpochMock.electionRewardDetails1,
|
||||
{ pathParams: { height_or_hash: heightOrHash, reward_type: 'voter' } },
|
||||
);
|
||||
const component = await render(<BlockEpochElectionRewards data={ blockEpochMock.blockEpoch1 }/>, { hooksConfig });
|
||||
await component.getByRole('cell', { name: 'Voting rewards' }).click();
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('base view +@mobile -@default', async({ render, mockApiResponse }) => {
|
||||
await mockApiResponse(
|
||||
'general:block_election_rewards',
|
||||
blockEpochMock.electionRewardDetails1,
|
||||
{ pathParams: { height_or_hash: heightOrHash, reward_type: 'voter' } },
|
||||
);
|
||||
const component = await render(<BlockEpochElectionRewards data={ blockEpochMock.blockEpoch1 }/>, { hooksConfig });
|
||||
await component.locator('div').filter({ hasText: 'Voting rewards' }).nth(3).click();
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
import { Grid } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { BlockEpoch } from 'types/api/block';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo';
|
||||
import TokenTransferSnippet from 'ui/shared/TokenTransferSnippet/TokenTransferSnippet';
|
||||
|
||||
interface Props {
|
||||
data: BlockEpoch;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const BlockEpochRewardsDistribution = ({ data, isLoading }: Props) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
if (!data.distribution) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 && (
|
||||
<>
|
||||
<DetailedInfo.ItemLabel
|
||||
hint="Funds allocation to support Celo projects and community initiatives"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
Community fund
|
||||
</DetailedInfo.ItemLabel>
|
||||
<DetailedInfo.ItemValue>
|
||||
<TokenTransferSnippet data={ data.distribution.community_transfer } isLoading={ isLoading } noAddressIcons={ isMobile }/>
|
||||
</DetailedInfo.ItemValue>
|
||||
</>
|
||||
) }
|
||||
{ data.distribution.carbon_offsetting_transfer && (
|
||||
<>
|
||||
<DetailedInfo.ItemLabel
|
||||
hint="Funds allocation to support projects that make Celo carbon-negative"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
Carbon offset fund
|
||||
</DetailedInfo.ItemLabel>
|
||||
<DetailedInfo.ItemValue>
|
||||
<TokenTransferSnippet data={ data.distribution.carbon_offsetting_transfer } isLoading={ isLoading } noAddressIcons={ isMobile }/>
|
||||
</DetailedInfo.ItemValue>
|
||||
</>
|
||||
) }
|
||||
{ data.distribution.reserve_bolster_transfer && (
|
||||
<>
|
||||
<DetailedInfo.ItemLabel
|
||||
hint="Funds allocation to strengthen Celo’s reserve for network stability and security"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
Reserve bolster
|
||||
</DetailedInfo.ItemLabel>
|
||||
<DetailedInfo.ItemValue>
|
||||
<TokenTransferSnippet data={ data.distribution.reserve_bolster_transfer } isLoading={ isLoading } noAddressIcons={ isMobile }/>
|
||||
</DetailedInfo.ItemValue>
|
||||
</>
|
||||
) }
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(BlockEpochRewardsDistribution);
|
||||
|
Before Width: | Height: | Size: 99 KiB |
@@ -115,7 +115,7 @@ const BlocksContent = ({ type, query, enableSocket = true, top }: Props) => {
|
||||
const actionBar = isMobile ? (
|
||||
<ActionBar mt={ -6 }>
|
||||
<Link href={ route({ pathname: '/block/countdown' }) }>
|
||||
<IconSvg name="hourglass" boxSize={ 5 } mr={ 2 }/>
|
||||
<IconSvg name="hourglass_slim" boxSize={ 5 } mr={ 2 }/>
|
||||
<span>Block countdown</span>
|
||||
</Link>
|
||||
<Pagination ml="auto" { ...query.pagination }/>
|
||||
|
||||
@@ -51,8 +51,8 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement, animation }: Pro
|
||||
noIcon
|
||||
fontWeight={ 600 }
|
||||
/>
|
||||
{ data.celo?.is_epoch_block && (
|
||||
<Tooltip content={ `Finalized epoch #${ data.celo.epoch_number }` } disabled={ isLoading }>
|
||||
{ data.celo?.l1_era_finalized_epoch_number && (
|
||||
<Tooltip content={ `Finalized epoch #${ data.celo.l1_era_finalized_epoch_number }` } disabled={ isLoading }>
|
||||
<IconSvg name="checkered_flag" boxSize={ 5 } p="1px" isLoading={ isLoading } flexShrink={ 0 }/>
|
||||
</Tooltip>
|
||||
) }
|
||||
|
||||
@@ -37,7 +37,7 @@ const BlocksTabSlot = ({ pagination }: Props) => {
|
||||
</Box>
|
||||
) }
|
||||
<Link href={ route({ pathname: '/block/countdown' }) }>
|
||||
<IconSvg name="hourglass" boxSize={ 5 } mr={ 2 }/>
|
||||
<IconSvg name="hourglass_slim" boxSize={ 5 } mr={ 2 }/>
|
||||
<span>Block countdown</span>
|
||||
</Link>
|
||||
<Pagination my={ 1 } { ...pagination }/>
|
||||
|
||||
@@ -41,8 +41,8 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement, animation }: Pr
|
||||
<TableRow animation={ animation }>
|
||||
<TableCell >
|
||||
<Flex columnGap={ 2 } alignItems="center" mb={ 2 }>
|
||||
{ data.celo?.is_epoch_block && (
|
||||
<Tooltip content={ `Finalized epoch #${ data.celo.epoch_number }` }>
|
||||
{ data.celo?.l1_era_finalized_epoch_number && (
|
||||
<Tooltip content={ `Finalized epoch #${ data.celo.l1_era_finalized_epoch_number }` }>
|
||||
<IconSvg name="checkered_flag" boxSize={ 5 } p="1px" isLoading={ isLoading } flexShrink={ 0 }/>
|
||||
</Tooltip>
|
||||
) }
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { Box, Grid, chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { CeloEpochDetails } from 'types/api/epochs';
|
||||
|
||||
import config from 'configs/app';
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo';
|
||||
import DetailedInfoTimestamp from 'ui/shared/DetailedInfo/DetailedInfoTimestamp';
|
||||
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
import CeloEpochStatus from 'ui/shared/statusTag/CeloEpochStatus';
|
||||
import TokenTransferSnippet from 'ui/shared/TokenTransferSnippet/TokenTransferSnippet';
|
||||
|
||||
import EpochElectionRewards from './electionRewards/EpochElectionRewards';
|
||||
|
||||
interface Props {
|
||||
data: CeloEpochDetails;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const EpochDetails = ({ data, isLoading }: Props) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const totalFunRewards = data.distribution?.transfers_total?.total ? getCurrencyValue({
|
||||
value: data.distribution?.transfers_total.total.value,
|
||||
decimals: data.distribution?.transfers_total.total.decimals,
|
||||
}) : null;
|
||||
|
||||
const processingRange = (() => {
|
||||
if (!data.start_processing_block_number || !data.end_processing_block_number) {
|
||||
return <Box color="text.secondary">N/A</Box>;
|
||||
}
|
||||
|
||||
if (data.start_processing_block_number === data.end_processing_block_number) {
|
||||
return <BlockEntity number={ data.start_processing_block_number } isLoading={ isLoading } noIcon/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BlockEntity number={ data.start_processing_block_number } isLoading={ isLoading } noIcon/>
|
||||
<chakra.span color="text.secondary" whiteSpace="pre"> - </chakra.span>
|
||||
<BlockEntity number={ data.end_processing_block_number } isLoading={ isLoading } noIcon/>
|
||||
</>
|
||||
);
|
||||
})();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid columnGap={ 8 } rowGap={ 3 } templateColumns={{ base: 'minmax(0, 1fr)', lg: 'max-content minmax(728px, auto)' }}>
|
||||
<DetailedInfo.ItemLabel
|
||||
hint="Current status of the epoch"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
Status
|
||||
</DetailedInfo.ItemLabel>
|
||||
<DetailedInfo.ItemValue>
|
||||
<CeloEpochStatus
|
||||
isFinalized={ data.is_finalized }
|
||||
loading={ isLoading }
|
||||
/>
|
||||
</DetailedInfo.ItemValue>
|
||||
<DetailedInfo.ItemLabel
|
||||
hint="Timestamp of the block where the epoch processing completed"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
Timestamp
|
||||
</DetailedInfo.ItemLabel>
|
||||
<DetailedInfo.ItemValue>
|
||||
{ data.timestamp ?
|
||||
<DetailedInfoTimestamp timestamp={ data.timestamp } isLoading={ isLoading }/> :
|
||||
<Box color="text.secondary" whiteSpace="pre-wrap">Epochs are finalized approximately once a day</Box> }
|
||||
</DetailedInfo.ItemValue>
|
||||
<DetailedInfo.ItemLabel
|
||||
// eslint-disable-next-line max-len
|
||||
hint={ `The range of blocks during which the epoch is processed — i.e., from the block where the "EpochProcessingStarted" event is emitted to the block where the "EpochProcessingEnded" event is emitted` }
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
Processing range
|
||||
</DetailedInfo.ItemLabel>
|
||||
<DetailedInfo.ItemValue>
|
||||
{ processingRange }
|
||||
</DetailedInfo.ItemValue>
|
||||
<DetailedInfo.ItemLabel
|
||||
hint="Funds allocation to support Celo projects and community initiatives"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
Community fund
|
||||
</DetailedInfo.ItemLabel>
|
||||
<DetailedInfo.ItemValue>
|
||||
{ data.distribution?.community_transfer ? (
|
||||
<TokenTransferSnippet
|
||||
data={ data.distribution.community_transfer }
|
||||
isLoading={ isLoading }
|
||||
noAddressIcons={ isMobile }
|
||||
/>
|
||||
) : (
|
||||
<Box color="text.secondary">N/A</Box>
|
||||
) }
|
||||
</DetailedInfo.ItemValue>
|
||||
<DetailedInfo.ItemLabel
|
||||
hint="Funds allocation to support projects that make Celo carbon-negative"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
Carbon offset fund
|
||||
</DetailedInfo.ItemLabel>
|
||||
<DetailedInfo.ItemValue>
|
||||
{ data.distribution?.carbon_offsetting_transfer ? (
|
||||
<TokenTransferSnippet
|
||||
data={ data.distribution.carbon_offsetting_transfer }
|
||||
isLoading={ isLoading }
|
||||
noAddressIcons={ isMobile }
|
||||
/>
|
||||
) : (
|
||||
<Box color="text.secondary">N/A</Box>
|
||||
) }
|
||||
</DetailedInfo.ItemValue>
|
||||
<DetailedInfo.ItemLabel
|
||||
hint="Sum of all fund allocations"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
Total fund rewards
|
||||
</DetailedInfo.ItemLabel>
|
||||
<DetailedInfo.ItemValue flexWrap="nowrap" gap={ 2 }>
|
||||
{ totalFunRewards ? (
|
||||
<>
|
||||
<Skeleton loading={ isLoading }>
|
||||
<span>{ totalFunRewards.valueStr }</span>
|
||||
</Skeleton>
|
||||
{ data.distribution?.transfers_total?.token ? (
|
||||
<TokenEntity
|
||||
token={ data.distribution?.transfers_total.token }
|
||||
isLoading={ isLoading }
|
||||
noCopy
|
||||
onlySymbol
|
||||
/>
|
||||
) :
|
||||
config.chain.currency.symbol }
|
||||
</>
|
||||
) : (
|
||||
<Box color="text.secondary">N/A</Box>
|
||||
) }
|
||||
</DetailedInfo.ItemValue>
|
||||
</Grid>
|
||||
<EpochElectionRewards data={ data } isLoading={ isLoading }/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(EpochDetails);
|
||||
@@ -0,0 +1,82 @@
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { CeloEpochListItem } from 'types/api/epochs';
|
||||
|
||||
import config from 'configs/app';
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
import DetailedInfoTimestamp from 'ui/shared/DetailedInfo/DetailedInfoTimestamp';
|
||||
import EpochEntity from 'ui/shared/entities/epoch/EpochEntity';
|
||||
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
|
||||
import CeloEpochStatus from 'ui/shared/statusTag/CeloEpochStatus';
|
||||
|
||||
interface Props {
|
||||
item: CeloEpochListItem;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const EpochsListItem = ({ item, isLoading }: Props) => {
|
||||
const communityReward = getCurrencyValue({
|
||||
value: item.distribution?.community_transfer?.value ?? '0',
|
||||
decimals: item.distribution?.community_transfer?.decimals,
|
||||
accuracy: 8,
|
||||
});
|
||||
const carbonOffsettingReward = getCurrencyValue({
|
||||
value: item.distribution?.carbon_offsetting_transfer?.value ?? '0',
|
||||
decimals: item.distribution?.carbon_offsetting_transfer?.decimals,
|
||||
accuracy: 8,
|
||||
});
|
||||
const totalReward = getCurrencyValue({
|
||||
value: item.distribution?.transfers_total?.value ?? '0',
|
||||
decimals: item.distribution?.transfers_total?.decimals,
|
||||
accuracy: 8,
|
||||
});
|
||||
|
||||
return (
|
||||
<ListItemMobile rowGap={ 1 } py={ 3 } w="full" textStyle="sm" fontWeight={ 500 } alignItems="stretch">
|
||||
<HStack minH="30px" gap={ 3 }>
|
||||
<EpochEntity number={ String(item.number) } isLoading={ isLoading }/>
|
||||
<Skeleton loading={ isLoading } color="text.secondary" fontWeight={ 400 } ml="auto"><span>{ item.type }</span></Skeleton>
|
||||
<CeloEpochStatus isFinalized={ item.is_finalized } loading={ isLoading }/>
|
||||
</HStack>
|
||||
{ item.timestamp && (
|
||||
<HStack minH="30px" gap={ 0 } color="text.secondary" fontWeight={ 400 }>
|
||||
<DetailedInfoTimestamp timestamp={ item.timestamp } isLoading={ isLoading } noIcon gap={ 1 }/>
|
||||
</HStack>
|
||||
) }
|
||||
<HStack minH="30px">
|
||||
<Skeleton loading={ isLoading }>Block range</Skeleton>
|
||||
<Skeleton loading={ isLoading } color="text.secondary">
|
||||
<span>{ item.start_block_number } - { item.end_block_number || '' }</span>
|
||||
</Skeleton>
|
||||
</HStack>
|
||||
{ item.distribution?.community_transfer ? (
|
||||
<HStack minH="30px">
|
||||
<Skeleton loading={ isLoading }>Community</Skeleton>
|
||||
<Skeleton loading={ isLoading } color="text.secondary">
|
||||
<span>{ communityReward.valueStr } { config.chain.currency.symbol }</span>
|
||||
</Skeleton>
|
||||
</HStack>
|
||||
) : null }
|
||||
{ item.distribution?.carbon_offsetting_transfer ? (
|
||||
<HStack minH="30px">
|
||||
<Skeleton loading={ isLoading }>Carbon offset</Skeleton>
|
||||
<Skeleton loading={ isLoading } color="text.secondary">
|
||||
<span>{ carbonOffsettingReward.valueStr } { config.chain.currency.symbol }</span>
|
||||
</Skeleton>
|
||||
</HStack>
|
||||
) : null }
|
||||
{ item.distribution?.transfers_total ? (
|
||||
<HStack minH="30px">
|
||||
<Skeleton loading={ isLoading }>Total</Skeleton>
|
||||
<Skeleton loading={ isLoading } color="text.secondary">
|
||||
<span>{ totalReward.valueStr } { config.chain.currency.symbol }</span>
|
||||
</Skeleton>
|
||||
</HStack>
|
||||
) : null }
|
||||
</ListItemMobile>
|
||||
);
|
||||
};
|
||||
|
||||
export default EpochsListItem;
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { CeloEpochListItem } from 'types/api/epochs';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table';
|
||||
import TimeFormatToggle from 'ui/shared/time/TimeFormatToggle';
|
||||
|
||||
import EpochsTableItem from './EpochsTableItem';
|
||||
|
||||
interface Props {
|
||||
items: Array<CeloEpochListItem>;
|
||||
isLoading?: boolean;
|
||||
top: number;
|
||||
};
|
||||
|
||||
const EpochsTable = ({ items, isLoading, top }: Props) => {
|
||||
return (
|
||||
<TableRoot minW="1100px">
|
||||
<TableHeaderSticky top={ top }>
|
||||
<TableRow>
|
||||
<TableColumnHeader w="280px">
|
||||
Epoch
|
||||
<TimeFormatToggle/>
|
||||
</TableColumnHeader>
|
||||
<TableColumnHeader w="120px">Status</TableColumnHeader>
|
||||
<TableColumnHeader w="25%">Block range</TableColumnHeader>
|
||||
<TableColumnHeader w="25%" isNumeric>Community { config.chain.currency.symbol }</TableColumnHeader>
|
||||
<TableColumnHeader w="25%" isNumeric>Carbon offset { config.chain.currency.symbol }</TableColumnHeader>
|
||||
<TableColumnHeader w="25%" isNumeric>Total { config.chain.currency.symbol }</TableColumnHeader>
|
||||
</TableRow>
|
||||
</TableHeaderSticky>
|
||||
<TableBody>
|
||||
{ items.map((item, index) => {
|
||||
return (
|
||||
<EpochsTableItem
|
||||
key={ item.number + (isLoading ? String(index) : '') }
|
||||
item={ item }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
);
|
||||
}) }
|
||||
</TableBody>
|
||||
</TableRoot>
|
||||
);
|
||||
};
|
||||
|
||||
export default EpochsTable;
|
||||
@@ -0,0 +1,81 @@
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { CeloEpochListItem } from 'types/api/epochs';
|
||||
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
import { TableCell, TableRow } from 'toolkit/chakra/table';
|
||||
import EpochEntity from 'ui/shared/entities/epoch/EpochEntity';
|
||||
import CeloEpochStatus from 'ui/shared/statusTag/CeloEpochStatus';
|
||||
import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip';
|
||||
|
||||
interface Props {
|
||||
item: CeloEpochListItem;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
const EpochsTableItem = ({ item, isLoading }: Props) => {
|
||||
|
||||
const communityReward = getCurrencyValue({
|
||||
value: item.distribution?.community_transfer?.value ?? '0',
|
||||
decimals: item.distribution?.community_transfer?.decimals,
|
||||
accuracy: 8,
|
||||
});
|
||||
const carbonOffsettingReward = getCurrencyValue({
|
||||
value: item.distribution?.carbon_offsetting_transfer?.value ?? '0',
|
||||
decimals: item.distribution?.carbon_offsetting_transfer?.decimals,
|
||||
accuracy: 8,
|
||||
});
|
||||
const totalReward = getCurrencyValue({
|
||||
value: item.distribution?.transfers_total?.value ?? '0',
|
||||
decimals: item.distribution?.transfers_total?.decimals,
|
||||
accuracy: 8,
|
||||
});
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell verticalAlign="middle">
|
||||
<HStack gap={ 2 }>
|
||||
<EpochEntity number={ String(item.number) } noIcon fontWeight={ 700 } isLoading={ isLoading }/>
|
||||
<Skeleton loading={ isLoading } color="text.secondary" fontWeight={ 500 }><span>{ item.type }</span></Skeleton>
|
||||
<TimeWithTooltip
|
||||
timestamp={ item.timestamp }
|
||||
isLoading={ isLoading }
|
||||
color="text.secondary"
|
||||
display="inline-block"
|
||||
fontWeight={ 400 }
|
||||
/>
|
||||
</HStack>
|
||||
</TableCell>
|
||||
<TableCell verticalAlign="middle">
|
||||
<CeloEpochStatus
|
||||
isFinalized={ item.is_finalized }
|
||||
loading={ isLoading }
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell verticalAlign="middle">
|
||||
<Skeleton loading={ isLoading }>
|
||||
{ item.start_block_number } - { item.end_block_number || '' }
|
||||
</Skeleton>
|
||||
</TableCell>
|
||||
<TableCell verticalAlign="middle" isNumeric>
|
||||
<Skeleton loading={ isLoading }>
|
||||
{ item.distribution?.community_transfer ? communityReward.valueStr : '-' }
|
||||
</Skeleton>
|
||||
</TableCell>
|
||||
<TableCell verticalAlign="middle" isNumeric>
|
||||
<Skeleton loading={ isLoading }>
|
||||
{ item.distribution?.carbon_offsetting_transfer ? carbonOffsettingReward.valueStr : '-' }
|
||||
</Skeleton>
|
||||
</TableCell>
|
||||
<TableCell verticalAlign="middle" isNumeric>
|
||||
<Skeleton loading={ isLoading }>
|
||||
{ item.distribution?.transfers_total ? totalReward.valueStr : '-' }
|
||||
</Skeleton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default EpochsTableItem;
|
||||
@@ -2,7 +2,7 @@ import { Box, Grid, GridItem, Text } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { BlockEpoch } from 'types/api/block';
|
||||
import type { CeloEpochDetails } from 'types/api/epochs';
|
||||
import type { TokenInfo } from 'types/api/token';
|
||||
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
@@ -14,20 +14,20 @@ import useLazyLoadedList from 'ui/shared/pagination/useLazyLoadedList';
|
||||
import { formatRewardType, getRewardDetailsTableTitles } from './utils';
|
||||
|
||||
interface Props {
|
||||
type: keyof BlockEpoch['aggregated_election_rewards'];
|
||||
type: keyof CeloEpochDetails['aggregated_election_rewards'];
|
||||
token: TokenInfo;
|
||||
}
|
||||
|
||||
const BlockEpochElectionRewardDetailsDesktop = ({ type, token }: Props) => {
|
||||
const CeloEpochElectionRewardDetailsDesktop = ({ type, token }: Props) => {
|
||||
const rootRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const heightOrHash = getQueryParamString(router.query.height_or_hash);
|
||||
const number = getQueryParamString(router.query.number);
|
||||
|
||||
const { cutRef, query } = useLazyLoadedList({
|
||||
rootRef,
|
||||
resourceName: 'general:block_election_rewards',
|
||||
pathParams: { height_or_hash: heightOrHash, reward_type: formatRewardType(type) },
|
||||
resourceName: 'general:epoch_celo_election_rewards',
|
||||
pathParams: { number: number, reward_type: formatRewardType(type) },
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
},
|
||||
@@ -97,4 +97,4 @@ const BlockEpochElectionRewardDetailsDesktop = ({ type, token }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(BlockEpochElectionRewardDetailsDesktop);
|
||||
export default React.memo(CeloEpochElectionRewardDetailsDesktop);
|
||||
@@ -2,7 +2,7 @@ import { Box, Flex, Text } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { BlockEpoch } from 'types/api/block';
|
||||
import type { CeloEpochDetails } from 'types/api/epochs';
|
||||
import type { TokenInfo } from 'types/api/token';
|
||||
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
@@ -15,20 +15,20 @@ import useLazyLoadedList from 'ui/shared/pagination/useLazyLoadedList';
|
||||
import { formatRewardType } from './utils';
|
||||
|
||||
interface Props {
|
||||
type: keyof BlockEpoch['aggregated_election_rewards'];
|
||||
type: keyof CeloEpochDetails['aggregated_election_rewards'];
|
||||
token: TokenInfo;
|
||||
}
|
||||
|
||||
const BlockEpochElectionRewardDetailsMobile = ({ type, token }: Props) => {
|
||||
const CeloEpochElectionRewardDetailsMobile = ({ type, token }: Props) => {
|
||||
const rootRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const heightOrHash = getQueryParamString(router.query.height_or_hash);
|
||||
const number = getQueryParamString(router.query.number);
|
||||
|
||||
const { cutRef, query } = useLazyLoadedList({
|
||||
rootRef,
|
||||
resourceName: 'general:block_election_rewards',
|
||||
pathParams: { height_or_hash: heightOrHash, reward_type: formatRewardType(type) },
|
||||
resourceName: 'general:epoch_celo_election_rewards',
|
||||
pathParams: { number: number, reward_type: formatRewardType(type) },
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
},
|
||||
@@ -80,4 +80,4 @@ const BlockEpochElectionRewardDetailsMobile = ({ type, token }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(BlockEpochElectionRewardDetailsMobile);
|
||||
export default React.memo(CeloEpochElectionRewardDetailsMobile);
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
|
||||
import * as celoEpochMock from 'mocks/epochs/celo';
|
||||
import { test, expect } from 'playwright/lib';
|
||||
|
||||
import EpochElectionRewards from './EpochElectionRewards';
|
||||
|
||||
const number = '1234';
|
||||
const hooksConfig = {
|
||||
router: {
|
||||
query: { number },
|
||||
},
|
||||
};
|
||||
|
||||
test('base view', async({ render, mockApiResponse }) => {
|
||||
await mockApiResponse(
|
||||
'general:epoch_celo_election_rewards',
|
||||
celoEpochMock.electionRewardDetails1,
|
||||
{ pathParams: { number, reward_type: 'voter' } },
|
||||
);
|
||||
const component = await render(<EpochElectionRewards data={ celoEpochMock.epoch1 }/>, { hooksConfig });
|
||||
await component.getByRole('cell', { name: 'Voting rewards' }).click();
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('base view +@mobile -@default', async({ render, mockApiResponse }) => {
|
||||
await mockApiResponse(
|
||||
'general:epoch_celo_election_rewards',
|
||||
celoEpochMock.electionRewardDetails1,
|
||||
{ pathParams: { number, reward_type: 'voter' } },
|
||||
);
|
||||
const component = await render(<EpochElectionRewards data={ celoEpochMock.epoch1 }/>, { hooksConfig });
|
||||
await component.locator('div').filter({ hasText: 'Voting rewards' }).nth(3).click();
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -1,26 +1,26 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { BlockEpoch } from 'types/api/block';
|
||||
import type { CeloEpochDetails } from 'types/api/epochs';
|
||||
|
||||
import { Heading } from 'toolkit/chakra/heading';
|
||||
import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table';
|
||||
|
||||
import BlockEpochElectionRewardsListItem from './BlockEpochElectionRewardsListItem';
|
||||
import BlockEpochElectionRewardsTableItem from './BlockEpochElectionRewardsTableItem';
|
||||
import EpochElectionRewardsListItem from './EpochElectionRewardsListItem';
|
||||
import EpochElectionRewardsTableItem from './EpochElectionRewardsTableItem';
|
||||
|
||||
interface Props {
|
||||
data: BlockEpoch;
|
||||
data: CeloEpochDetails;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const BlockEpochElectionRewards = ({ data, isLoading }: Props) => {
|
||||
const EpochElectionRewards = ({ data, isLoading }: Props) => {
|
||||
if (!data.aggregated_election_rewards) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box mt={ 8 }>
|
||||
<Box mt={ 6 }>
|
||||
<Heading level="3" mb={ 3 }>Election rewards</Heading>
|
||||
<Box hideBelow="lg">
|
||||
<TableRoot style={{ tableLayout: 'auto' }}>
|
||||
@@ -34,7 +34,7 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => {
|
||||
</TableHeaderSticky>
|
||||
<TableBody>
|
||||
{ Object.entries(data.aggregated_election_rewards).map((entry) => {
|
||||
const key = entry[0] as keyof BlockEpoch['aggregated_election_rewards'];
|
||||
const key = entry[0] as keyof CeloEpochDetails['aggregated_election_rewards'];
|
||||
const value = entry[1];
|
||||
|
||||
if (!value) {
|
||||
@@ -42,7 +42,7 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<BlockEpochElectionRewardsTableItem
|
||||
<EpochElectionRewardsTableItem
|
||||
key={ key }
|
||||
type={ key }
|
||||
isLoading={ isLoading }
|
||||
@@ -55,7 +55,7 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => {
|
||||
</Box>
|
||||
<Box hideFrom="lg">
|
||||
{ Object.entries(data.aggregated_election_rewards).map((entry) => {
|
||||
const key = entry[0] as keyof BlockEpoch['aggregated_election_rewards'];
|
||||
const key = entry[0] as keyof CeloEpochDetails['aggregated_election_rewards'];
|
||||
const value = entry[1];
|
||||
|
||||
if (!value) {
|
||||
@@ -63,7 +63,7 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<BlockEpochElectionRewardsListItem
|
||||
<EpochElectionRewardsListItem
|
||||
key={ key }
|
||||
type={ key }
|
||||
isLoading={ isLoading }
|
||||
@@ -76,4 +76,4 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(BlockEpochElectionRewards);
|
||||
export default React.memo(EpochElectionRewards);
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { BlockEpoch, BlockEpochElectionReward } from 'types/api/block';
|
||||
import type { CeloEpochElectionReward, CeloEpochDetails } from 'types/api/epochs';
|
||||
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
import { IconButton } from 'toolkit/chakra/icon-button';
|
||||
@@ -11,15 +11,16 @@ import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import BlockEpochElectionRewardDetailsMobile from './BlockEpochElectionRewardDetailsMobile';
|
||||
import EpochElectionRewardDetailsMobile from './EpochElectionRewardDetailsMobile';
|
||||
import { getRewardNumText } from './utils';
|
||||
|
||||
interface Props {
|
||||
data: BlockEpochElectionReward;
|
||||
type: keyof BlockEpoch['aggregated_election_rewards'];
|
||||
data: CeloEpochElectionReward;
|
||||
type: keyof CeloEpochDetails['aggregated_election_rewards'];
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const BlockEpochElectionRewardsListItem = ({ data, isLoading, type }: Props) => {
|
||||
const EpochElectionRewardsListItem = ({ data, isLoading, type }: Props) => {
|
||||
const section = useDisclosure();
|
||||
|
||||
const { valueStr } = getCurrencyValue({
|
||||
@@ -55,7 +56,7 @@ const BlockEpochElectionRewardsListItem = ({ data, isLoading, type }: Props) =>
|
||||
</Skeleton>
|
||||
) : <Box boxSize={ 6 }/> }
|
||||
<EpochRewardTypeTag type={ type } isLoading={ isLoading }/>
|
||||
<Skeleton loading={ isLoading }>{ data.count }</Skeleton>
|
||||
<Skeleton loading={ isLoading } ml="auto">{ getRewardNumText(type, data.count) }</Skeleton>
|
||||
<Flex columnGap={ 2 } alignItems="center" ml={{ base: 9, lg: 'auto' }} w={{ base: '100%', lg: 'fit-content' }} fontWeight={ 500 }>
|
||||
<Skeleton loading={ isLoading }>{ valueStr }</Skeleton>
|
||||
<TokenEntity
|
||||
@@ -69,11 +70,11 @@ const BlockEpochElectionRewardsListItem = ({ data, isLoading, type }: Props) =>
|
||||
</Flex>
|
||||
{ section.open && (
|
||||
<Box mt={ 2 }>
|
||||
<BlockEpochElectionRewardDetailsMobile type={ type } token={ data.token }/>
|
||||
<EpochElectionRewardDetailsMobile type={ type } token={ data.token }/>
|
||||
</Box>
|
||||
) }
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(BlockEpochElectionRewardsListItem);
|
||||
export default React.memo(EpochElectionRewardsListItem);
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { BlockEpoch, BlockEpochElectionReward } from 'types/api/block';
|
||||
import type { CeloEpochDetails, CeloEpochElectionReward } from 'types/api/epochs';
|
||||
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
import { IconButton } from 'toolkit/chakra/icon-button';
|
||||
@@ -12,16 +12,16 @@ import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import BlockEpochElectionRewardDetailsDesktop from './BlockEpochElectionRewardDetailsDesktop';
|
||||
import EpochElectionRewardDetailsDesktop from './EpochElectionRewardDetailsDesktop';
|
||||
import { getRewardNumText } from './utils';
|
||||
|
||||
interface Props {
|
||||
data: BlockEpochElectionReward;
|
||||
type: keyof BlockEpoch['aggregated_election_rewards'];
|
||||
data: CeloEpochElectionReward;
|
||||
type: keyof CeloEpochDetails['aggregated_election_rewards'];
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const BlockEpochElectionRewardsTableItem = ({ isLoading, data, type }: Props) => {
|
||||
const EpochElectionRewardsTableItem = ({ isLoading, data, type }: Props) => {
|
||||
const section = useDisclosure();
|
||||
|
||||
const { valueStr } = getCurrencyValue({
|
||||
@@ -79,7 +79,7 @@ const BlockEpochElectionRewardsTableItem = ({ isLoading, data, type }: Props) =>
|
||||
<TableRow>
|
||||
<TableCell/>
|
||||
<TableCell colSpan={ 3 } pr={ 0 } pt={ 0 }>
|
||||
<BlockEpochElectionRewardDetailsDesktop type={ type } token={ data.token }/>
|
||||
<EpochElectionRewardDetailsDesktop type={ type } token={ data.token }/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) }
|
||||
@@ -87,4 +87,4 @@ const BlockEpochElectionRewardsTableItem = ({ isLoading, data, type }: Props) =>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(BlockEpochElectionRewardsTableItem);
|
||||
export default React.memo(EpochElectionRewardsTableItem);
|
||||
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 106 KiB |
@@ -1,7 +1,7 @@
|
||||
import type { BlockEpoch } from 'types/api/block';
|
||||
import type { CeloEpochDetails } from 'types/api/epochs';
|
||||
import type { ExcludeNull } from 'types/utils';
|
||||
|
||||
export function getRewardNumText(type: keyof BlockEpoch['aggregated_election_rewards'], num: number) {
|
||||
export function getRewardNumText(type: keyof CeloEpochDetails['aggregated_election_rewards'], num: number) {
|
||||
const postfix1 = num !== 1 ? 's' : '';
|
||||
const postfix2 = num !== 1 ? 'es' : '';
|
||||
|
||||
@@ -27,7 +27,7 @@ export function getRewardNumText(type: keyof BlockEpoch['aggregated_election_rew
|
||||
return `${ num } ${ text }`;
|
||||
}
|
||||
|
||||
export function getRewardDetailsTableTitles(type: keyof ExcludeNull<BlockEpoch['aggregated_election_rewards']>): [string, string] {
|
||||
export function getRewardDetailsTableTitles(type: keyof ExcludeNull<CeloEpochDetails['aggregated_election_rewards']>): [string, string] {
|
||||
switch (type) {
|
||||
case 'delegated_payment':
|
||||
return [ 'Beneficiary', 'Validator' ];
|
||||
@@ -40,6 +40,6 @@ export function getRewardDetailsTableTitles(type: keyof ExcludeNull<BlockEpoch['
|
||||
}
|
||||
}
|
||||
|
||||
export function formatRewardType(type: keyof ExcludeNull<BlockEpoch['aggregated_election_rewards']>) {
|
||||
export function formatRewardType(type: keyof ExcludeNull<CeloEpochDetails['aggregated_election_rewards']>) {
|
||||
return type.replaceAll('_', '-');
|
||||
}
|
||||
@@ -38,8 +38,8 @@ const LatestBlocksItem = ({ block, isLoading, animation }: Props) => {
|
||||
fontWeight={ 500 }
|
||||
mr="auto"
|
||||
/>
|
||||
{ block.celo?.is_epoch_block && (
|
||||
<Tooltip content={ `Finalized epoch #${ block.celo.epoch_number }` }>
|
||||
{ block.celo?.l1_era_finalized_epoch_number && (
|
||||
<Tooltip content={ `Finalized epoch #${ block.celo.l1_era_finalized_epoch_number }` }>
|
||||
<IconSvg name="checkered_flag" boxSize={ 5 } p="1px" ml={ 2 } isLoading={ isLoading } flexShrink={ 0 }/>
|
||||
</Tooltip>
|
||||
) }
|
||||
|
||||
@@ -200,9 +200,10 @@ const Stats = () => {
|
||||
},
|
||||
apiData?.celo && {
|
||||
id: 'current_epoch' as const,
|
||||
icon: 'hourglass' as const,
|
||||
icon: 'hourglass_slim' as const,
|
||||
label: 'Current epoch',
|
||||
value: `#${ apiData.celo.epoch_number }`,
|
||||
href: { pathname: '/epochs/[number]' as const, query: { number: String(apiData.celo.epoch_number) } },
|
||||
isLoading,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -17,7 +17,6 @@ import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
|
||||
import BlockCeloEpochTag from 'ui/block/BlockCeloEpochTag';
|
||||
import BlockDetails from 'ui/block/BlockDetails';
|
||||
import BlockEpochRewards from 'ui/block/BlockEpochRewards';
|
||||
import BlockInternalTxs from 'ui/block/BlockInternalTxs';
|
||||
import BlockWithdrawals from 'ui/block/BlockWithdrawals';
|
||||
import useBlockBlobTxsQuery from 'ui/block/useBlockBlobTxsQuery';
|
||||
@@ -110,12 +109,7 @@ const BlockPageContent = () => {
|
||||
</>
|
||||
),
|
||||
} : null,
|
||||
blockQuery.data?.celo?.is_epoch_block ? {
|
||||
id: 'epoch_rewards',
|
||||
title: 'Epoch rewards',
|
||||
component: <BlockEpochRewards heightOrHash={ heightOrHash }/>,
|
||||
} : null,
|
||||
].filter(Boolean)), [ blockBlobTxsQuery, blockInternalTxsQuery, blockQuery, blockTxsQuery, blockWithdrawalsQuery, hasPagination, heightOrHash ]);
|
||||
].filter(Boolean)), [ blockBlobTxsQuery, blockInternalTxsQuery, blockQuery, blockTxsQuery, blockWithdrawalsQuery, hasPagination ]);
|
||||
|
||||
let pagination;
|
||||
if (tab === 'txs') {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
|
||||
import * as epochMock from 'mocks/epochs/celo';
|
||||
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
|
||||
import { test, expect } from 'playwright/lib';
|
||||
|
||||
import Epoch from './Epoch';
|
||||
|
||||
test('base view +@mobile', async({ render, mockEnvs, mockTextAd, mockApiResponse }) => {
|
||||
const hooksConfig = {
|
||||
router: {
|
||||
query: { number: String(epochMock.epoch1.number) },
|
||||
},
|
||||
};
|
||||
|
||||
await mockEnvs(ENVS_MAP.celo);
|
||||
await mockTextAd();
|
||||
await mockApiResponse('general:epoch_celo', epochMock.epoch1, { pathParams: { number: String(epochMock.epoch1.number) } });
|
||||
|
||||
const component = await render(<Epoch/>, { hooksConfig });
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('unfinalized epoch', async({ render, mockEnvs, mockTextAd, mockApiResponse }) => {
|
||||
const hooksConfig = {
|
||||
router: {
|
||||
query: { number: String(epochMock.epochUnfinalized.number) },
|
||||
},
|
||||
};
|
||||
|
||||
await mockEnvs(ENVS_MAP.celo);
|
||||
await mockTextAd();
|
||||
await mockApiResponse('general:epoch_celo', epochMock.epochUnfinalized, { pathParams: { number: String(epochMock.epochUnfinalized.number) } });
|
||||
|
||||
const component = await render(<Epoch/>, { hooksConfig });
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Box, HStack } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import { useAppContext } from 'lib/contexts/app';
|
||||
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import { CELO_EPOCH } from 'stubs/epoch';
|
||||
import { Tag } from 'toolkit/chakra/tag';
|
||||
import { Tooltip } from 'toolkit/chakra/tooltip';
|
||||
import EpochDetails from 'ui/epochs/EpochDetails';
|
||||
import TextAd from 'ui/shared/ad/TextAd';
|
||||
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
|
||||
const EpochPageContent = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const appProps = useAppContext();
|
||||
const router = useRouter();
|
||||
const number = getQueryParamString(router.query.number);
|
||||
|
||||
const epochQuery = useApiQuery('general:epoch_celo', {
|
||||
pathParams: {
|
||||
number: number,
|
||||
},
|
||||
queryOptions: {
|
||||
placeholderData: CELO_EPOCH,
|
||||
},
|
||||
});
|
||||
|
||||
throwOnResourceLoadError(epochQuery);
|
||||
|
||||
const isLoading = epochQuery.isPlaceholderData;
|
||||
|
||||
const backLink = React.useMemo(() => {
|
||||
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/epochs');
|
||||
|
||||
if (!hasGoBackLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
label: 'Back to epochs list',
|
||||
url: appProps.referrer,
|
||||
};
|
||||
}, [ appProps.referrer ]);
|
||||
|
||||
const titleContentAfter = (() => {
|
||||
switch (epochQuery.data?.type) {
|
||||
case 'L1':
|
||||
return (
|
||||
<Tooltip content="Epoch finalized while Celo was still an L1 network">
|
||||
<Tag loading={ isLoading }>{ epochQuery.data.type }</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
case 'L2':
|
||||
return (
|
||||
<Tooltip content="Epoch finalized after Celo migrated to the OP‐stack, when it became an L2 rollup">
|
||||
<Tag loading={ isLoading }>{ epochQuery.data.type }</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
|
||||
const titleSecondRow = (() => {
|
||||
if (!epochQuery.data || epochQuery.data?.start_block_number === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isTruncated = isMobile && Boolean(epochQuery.data.end_block_number);
|
||||
const truncationProps = isTruncated ? { truncation: 'constant' as const, truncationMaxSymbols: 6 } : undefined;
|
||||
|
||||
return (
|
||||
<HStack textStyle={{ base: 'heading.sm', lg: 'heading.md' }} flexWrap="wrap">
|
||||
<Box color="text.secondary">Ranging from</Box>
|
||||
<BlockEntity
|
||||
number={ epochQuery.data.start_block_number }
|
||||
variant="subheading"
|
||||
{ ...truncationProps }
|
||||
/>
|
||||
{ epochQuery.data.end_block_number && (
|
||||
<>
|
||||
<Box color="text.secondary">to</Box>
|
||||
<BlockEntity number={ epochQuery.data.end_block_number } variant="subheading" { ...truncationProps }/>
|
||||
</>
|
||||
) }
|
||||
</HStack>
|
||||
);
|
||||
})();
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextAd mb={ 6 }/>
|
||||
<PageTitle
|
||||
title={ `Epoch #${ number }` }
|
||||
backLink={ backLink }
|
||||
contentAfter={ titleContentAfter }
|
||||
secondRow={ titleSecondRow }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
{ epochQuery.data && <EpochDetails data={ epochQuery.data } isLoading={ isLoading }/> }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EpochPageContent;
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
import { list as epochsList } from 'mocks/epochs/celo';
|
||||
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
|
||||
import { test, expect } from 'playwright/lib';
|
||||
|
||||
import Epochs from './Epochs';
|
||||
|
||||
test('base view +@mobile', async({ render, mockEnvs, mockTextAd, mockApiResponse }) => {
|
||||
await mockEnvs(ENVS_MAP.celo);
|
||||
await mockTextAd();
|
||||
await mockApiResponse('general:epochs_celo', epochsList);
|
||||
|
||||
const component = await render(<Epochs/>);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import { CELO_EPOCH_ITEM } from 'stubs/epoch';
|
||||
import { generateListStub } from 'stubs/utils';
|
||||
import EpochsListItem from 'ui/epochs/EpochsListItem';
|
||||
import EpochsTable from 'ui/epochs/EpochsTable';
|
||||
import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
|
||||
import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import Pagination from 'ui/shared/pagination/Pagination';
|
||||
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
|
||||
|
||||
const EpochsPageContent = () => {
|
||||
const epochsQuery = useQueryWithPages({
|
||||
resourceName: 'general:epochs_celo',
|
||||
options: {
|
||||
placeholderData: generateListStub<'general:epochs_celo'>(CELO_EPOCH_ITEM, 50, { next_page_params: {
|
||||
number: 1739,
|
||||
items_count: 50,
|
||||
} }),
|
||||
},
|
||||
});
|
||||
|
||||
const actionBar = epochsQuery.pagination.isVisible ? (
|
||||
<ActionBar mt={ -6 }>
|
||||
<Pagination ml="auto" { ...epochsQuery.pagination }/>
|
||||
</ActionBar>
|
||||
) : null;
|
||||
|
||||
const isLoading = epochsQuery.isPlaceholderData;
|
||||
|
||||
const content = (() => {
|
||||
if (epochsQuery.isError) {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
return epochsQuery.data?.items ? (
|
||||
<>
|
||||
<Box hideBelow="lg">
|
||||
<EpochsTable
|
||||
items={ epochsQuery.data.items }
|
||||
top={ epochsQuery.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
</Box>
|
||||
<Box hideFrom="lg">
|
||||
{ epochsQuery.data.items.map((item, index) => (
|
||||
<EpochsListItem
|
||||
key={ item.number + (epochsQuery.isPlaceholderData ? String(index) : '') }
|
||||
item={ item }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
)) }
|
||||
</Box>
|
||||
</>
|
||||
) : null;
|
||||
})();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle title="Epochs" withTextAd/>
|
||||
<DataListDisplay
|
||||
isError={ epochsQuery.isError }
|
||||
itemsNum={ epochsQuery.data?.items?.length }
|
||||
emptyText="There are no epochs."
|
||||
actionBar={ actionBar }
|
||||
>
|
||||
{ content }
|
||||
</DataListDisplay>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EpochsPageContent;
|
||||
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 51 KiB |
@@ -10,16 +10,17 @@ type Props = {
|
||||
timestamp: string | number;
|
||||
isLoading?: boolean;
|
||||
noIcon?: boolean;
|
||||
gap?: number;
|
||||
};
|
||||
|
||||
const DetailedInfoTimestamp = ({ timestamp, isLoading, noIcon }: Props) => {
|
||||
const DetailedInfoTimestamp = ({ timestamp, isLoading, noIcon, gap }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{ !noIcon && <IconSvg name="clock" boxSize={ 5 } color="gray.500" isLoading={ isLoading } mr={ 2 }/> }
|
||||
<Skeleton loading={ isLoading }>
|
||||
{ dayjs(timestamp).fromNow() }
|
||||
</Skeleton>
|
||||
<TextSeparator color="gray.500"/>
|
||||
<TextSeparator color="gray.500" mx={ gap ?? 3 }/>
|
||||
<Skeleton loading={ isLoading } whiteSpace="normal">
|
||||
{ dayjs(timestamp).format('llll') }
|
||||
</Skeleton>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { EpochRewardsType } from 'types/api/block';
|
||||
import type { CeloEpochRewardsType } from 'types/api/epochs';
|
||||
|
||||
import type { BadgeProps } from 'toolkit/chakra/badge';
|
||||
import { Badge } from 'toolkit/chakra/badge';
|
||||
import { Tooltip } from 'toolkit/chakra/tooltip';
|
||||
|
||||
type Props = {
|
||||
type: EpochRewardsType;
|
||||
type: CeloEpochRewardsType;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
const TYPE_TAGS: Record<EpochRewardsType, { text: string; label: string; color: BadgeProps['colorPalette'] }> = {
|
||||
const TYPE_TAGS: Record<CeloEpochRewardsType, { text: string; label: string; color: BadgeProps['colorPalette'] }> = {
|
||||
group: {
|
||||
text: 'Validator group rewards',
|
||||
// eslint-disable-next-line max-len
|
||||
|
||||
@@ -9,11 +9,12 @@ interface Props {
|
||||
noTooltip?: boolean;
|
||||
tooltipInteractive?: boolean;
|
||||
type?: 'long' | 'short';
|
||||
maxSymbols?: number;
|
||||
as?: React.ElementType;
|
||||
}
|
||||
|
||||
const HashStringShorten = ({ hash, noTooltip, as = 'span', type, tooltipInteractive }: Props) => {
|
||||
const charNumber = type === 'long' ? 16 : 8;
|
||||
const HashStringShorten = ({ hash, noTooltip, as = 'span', type, tooltipInteractive, maxSymbols }: Props) => {
|
||||
const charNumber = maxSymbols ?? (type === 'long' ? 16 : 8);
|
||||
if (hash.length <= charNumber) {
|
||||
return <chakra.span as={ as }>{ hash }</chakra.span>;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface EntityBaseProps {
|
||||
tailLength?: number;
|
||||
target?: React.HTMLAttributeAnchorTarget;
|
||||
truncation?: Truncation;
|
||||
truncationMaxSymbols?: number;
|
||||
variant?: 'content' | 'heading' | 'subheading';
|
||||
linkVariant?: LinkProps['variant'];
|
||||
}
|
||||
@@ -167,7 +168,9 @@ const IconShield = (props: IconShieldProps) => {
|
||||
return <IconSvg className="entity__shield" { ...styles } { ...svgProps }/>;
|
||||
};
|
||||
|
||||
export interface ContentBaseProps extends Pick<EntityBaseProps, 'className' | 'isLoading' | 'truncation' | 'tailLength' | 'noTooltip' | 'variant'> {
|
||||
export interface ContentBaseProps extends Pick<
|
||||
EntityBaseProps, 'className' | 'isLoading' | 'truncation' | 'tailLength' | 'noTooltip' | 'variant' | 'truncationMaxSymbols'
|
||||
> {
|
||||
asProp?: React.ElementType;
|
||||
text: string;
|
||||
tooltipInteractive?: boolean;
|
||||
@@ -179,6 +182,7 @@ const Content = chakra(({
|
||||
asProp,
|
||||
text,
|
||||
truncation = 'dynamic',
|
||||
truncationMaxSymbols,
|
||||
tailLength,
|
||||
variant,
|
||||
noTooltip,
|
||||
@@ -208,6 +212,7 @@ const Content = chakra(({
|
||||
type="long"
|
||||
noTooltip={ noTooltip }
|
||||
tooltipInteractive={ tooltipInteractive }
|
||||
maxSymbols={ truncationMaxSymbols }
|
||||
/>
|
||||
);
|
||||
case 'constant':
|
||||
@@ -217,6 +222,7 @@ const Content = chakra(({
|
||||
as={ asProp }
|
||||
noTooltip={ noTooltip }
|
||||
tooltipInteractive={ tooltipInteractive }
|
||||
maxSymbols={ truncationMaxSymbols }
|
||||
/>
|
||||
);
|
||||
case 'dynamic':
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import * as EntityBase from 'ui/shared/entities/base/components';
|
||||
|
||||
import { distributeEntityProps } from '../base/utils';
|
||||
|
||||
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'number'>;
|
||||
|
||||
const Link = chakra((props: LinkProps) => {
|
||||
const defaultHref = route({ pathname: '/epochs/[number]', query: { number: props.number } });
|
||||
|
||||
return (
|
||||
<EntityBase.Link
|
||||
{ ...props }
|
||||
href={ props.href ?? defaultHref }
|
||||
>
|
||||
{ props.children }
|
||||
</EntityBase.Link>
|
||||
);
|
||||
});
|
||||
|
||||
const Icon = (props: EntityBase.IconBaseProps) => {
|
||||
return (
|
||||
<EntityBase.Icon
|
||||
{ ...props }
|
||||
name={ props.name ?? 'hourglass_slim' }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'number'>;
|
||||
|
||||
const Content = chakra((props: ContentProps) => {
|
||||
return (
|
||||
<EntityBase.Content
|
||||
{ ...props }
|
||||
text={ props.number }
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
type CopyProps = Omit<EntityBase.CopyBaseProps, 'text'> & Pick<EntityProps, 'number'>;
|
||||
|
||||
const Copy = (props: CopyProps) => {
|
||||
return (
|
||||
<EntityBase.Copy
|
||||
{ ...props }
|
||||
text={ props.number }
|
||||
// by default we don't show copy icon
|
||||
noCopy={ props.noCopy ?? true }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Container = EntityBase.Container;
|
||||
|
||||
export interface EntityProps extends EntityBase.EntityBaseProps {
|
||||
number: string;
|
||||
}
|
||||
|
||||
const EpochEntity = (props: EntityProps) => {
|
||||
const partsProps = distributeEntityProps(props);
|
||||
const content = <Content { ...partsProps.content }/>;
|
||||
|
||||
return (
|
||||
<Container { ...partsProps.container }>
|
||||
<Icon { ...partsProps.icon }/>
|
||||
{ props.noLink ? content : <Link { ...partsProps.link }>{ content }</Link> }
|
||||
<Copy { ...partsProps.copy }/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(chakra(EpochEntity));
|
||||
|
||||
export {
|
||||
Container,
|
||||
Link,
|
||||
Icon,
|
||||
Content,
|
||||
Copy,
|
||||
};
|
||||
@@ -26,10 +26,10 @@ export type Props = {
|
||||
icon?: IconName;
|
||||
};
|
||||
|
||||
const Container = ({ href, children }: { href?: Route; children: React.JSX.Element }) => {
|
||||
const Container = ({ href, children, className }: { href?: Route; children: React.JSX.Element; className?: string }) => {
|
||||
if (href) {
|
||||
return (
|
||||
<Link href={ route(href) } variant="plain">
|
||||
<Link href={ route(href) } variant="plain" className={ className }>
|
||||
{ children }
|
||||
</Link>
|
||||
);
|
||||
@@ -54,9 +54,9 @@ const StatsWidget = ({
|
||||
href,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Container href={ !isLoading ? href : undefined }>
|
||||
<Container href={ !isLoading ? href : undefined } className={ href ? className : undefined }>
|
||||
<Flex
|
||||
className={ className }
|
||||
className={ href ? undefined : className }
|
||||
alignItems="center"
|
||||
bgColor={ isLoading ? { _light: 'blackAlpha.50', _dark: 'whiteAlpha.50' } : { _light: 'gray.50', _dark: 'whiteAlpha.100' } }
|
||||
p={ 3 }
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { Props as StatusTagProps } from './StatusTag';
|
||||
import StatusTag from './StatusTag';
|
||||
|
||||
export interface Props extends Omit<StatusTagProps, 'type' | 'text'> {
|
||||
isFinalized: boolean;
|
||||
}
|
||||
|
||||
const CeloEpochStatus = ({ isFinalized, ...rest }: Props) => {
|
||||
return (
|
||||
<StatusTag
|
||||
{ ...rest }
|
||||
type={ isFinalized ? 'ok' : 'pending' }
|
||||
text={ isFinalized ? 'Finalized' : 'In progress' }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CeloEpochStatus;
|
||||