Commit ff04dc85 authored by tom goriunov's avatar tom goriunov Committed by GitHub

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
parent 84663f2f
...@@ -4,14 +4,12 @@ import { getEnvValue } from '../utils'; ...@@ -4,14 +4,12 @@ import { getEnvValue } from '../utils';
const title = 'Celo chain'; const title = 'Celo chain';
const config: Feature<{ L2UpgradeBlock: number | undefined; BLOCKS_PER_EPOCH: number }> = (() => { const config: Feature<{ }> = (() => {
if (getEnvValue('NEXT_PUBLIC_CELO_ENABLED') === 'true') { if (getEnvValue('NEXT_PUBLIC_CELO_ENABLED') === 'true') {
return Object.freeze({ return Object.freeze({
title, title,
isEnabled: true, 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=/ ...@@ -15,7 +15,6 @@ NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=celo-alfajores.blockscout.com 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_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_CELO_ENABLED=true 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_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_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 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 ...@@ -24,6 +23,7 @@ NEXT_PUBLIC_GAS_TRACKER_ENABLED=false
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x9767ce30754afad2a3279b9df2d13257f467c3dad4e0e601271e66d16dfd1641 NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x9767ce30754afad2a3279b9df2d13257f467c3dad4e0e601271e66d16dfd1641
NEXT_PUBLIC_HAS_USER_OPS=true NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] 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_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(252, 255, 82, 1)'],'text_color':['rgba(0, 0, 0, 1)']}
NEXT_PUBLIC_IS_TESTNET=true NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_MARKETPLACE_ENABLED=false NEXT_PUBLIC_MARKETPLACE_ENABLED=false
......
...@@ -448,16 +448,6 @@ const celoSchema = yup ...@@ -448,16 +448,6 @@ const celoSchema = yup
.object() .object()
.shape({ .shape({
NEXT_PUBLIC_CELO_ENABLED: yup.boolean(), 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 const adButlerConfigSchema = yup
......
NEXT_PUBLIC_CELO_ENABLED=true NEXT_PUBLIC_CELO_ENABLED=true
NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK=420 \ No newline at end of file
\ No newline at end of file
...@@ -12,3 +12,4 @@ ...@@ -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_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_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_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 ...@@ -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 | | 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_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+ |
&nbsp; &nbsp;
......
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <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> </svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 30 30" 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"/> <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> </svg>
<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>
...@@ -116,7 +116,7 @@ export const GENERAL_API_ADDRESS_RESOURCES = { ...@@ -116,7 +116,7 @@ export const GENERAL_API_ADDRESS_RESOURCES = {
paginated: true, paginated: true,
}, },
address_epoch_rewards: { address_epoch_rewards: {
path: '/api/v2/addresses/:hash/election-rewards', path: '/api/v2/addresses/:hash/celo/election-rewards',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
filterFields: [], filterFields: [],
paginated: true, paginated: true,
......
...@@ -6,8 +6,6 @@ import type { ...@@ -6,8 +6,6 @@ import type {
BlockFilters, BlockFilters,
BlockWithdrawalsResponse, BlockWithdrawalsResponse,
BlockCountdownResponse, BlockCountdownResponse,
BlockEpoch,
BlockEpochElectionRewardDetailsResponse,
BlockInternalTransactionsResponse, BlockInternalTransactionsResponse,
} from 'types/api/block'; } from 'types/api/block';
import type { TTxsWithBlobsFilters } from 'types/api/txsFilters'; import type { TTxsWithBlobsFilters } from 'types/api/txsFilters';
...@@ -39,17 +37,6 @@ export const GENERAL_API_BLOCK_RESOURCES = { ...@@ -39,17 +37,6 @@ export const GENERAL_API_BLOCK_RESOURCES = {
filterFields: [], filterFields: [],
paginated: true, 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>; } satisfies Record<string, ApiResource>;
export type GeneralApiBlockResourceName = `general:${ keyof typeof GENERAL_API_BLOCK_RESOURCES }`; export type GeneralApiBlockResourceName = `general:${ keyof typeof GENERAL_API_BLOCK_RESOURCES }`;
...@@ -62,8 +49,6 @@ R extends 'general:block_countdown' ? BlockCountdownResponse : ...@@ -62,8 +49,6 @@ R extends 'general:block_countdown' ? BlockCountdownResponse :
R extends 'general:block_txs' ? BlockTransactionsResponse : R extends 'general:block_txs' ? BlockTransactionsResponse :
R extends 'general:block_internal_txs' ? BlockInternalTransactionsResponse : R extends 'general:block_internal_txs' ? BlockInternalTransactionsResponse :
R extends 'general:block_withdrawals' ? BlockWithdrawalsResponse : R extends 'general:block_withdrawals' ? BlockWithdrawalsResponse :
R extends 'general:block_epoch' ? BlockEpoch :
R extends 'general:block_election_rewards' ? BlockEpochElectionRewardDetailsResponse :
never; never;
/* eslint-enable @stylistic/indent */ /* eslint-enable @stylistic/indent */
......
...@@ -7,7 +7,8 @@ import type { ...@@ -7,7 +7,8 @@ import type {
import type { Blob } from 'types/api/blobs'; import type { Blob } from 'types/api/blobs';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts'; 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 { IndexingStatus } from 'types/api/indexingStatus';
import type { NovesAccountHistoryResponse, NovesDescribeTxsResponse, NovesResponseData } from 'types/api/noves'; import type { NovesAccountHistoryResponse, NovesDescribeTxsResponse, NovesResponseData } from 'types/api/noves';
import type { import type {
...@@ -181,6 +182,23 @@ export const GENERAL_API_MISC_RESOURCES = { ...@@ -181,6 +182,23 @@ export const GENERAL_API_MISC_RESOURCES = {
pathParams: [ 'hash' as const ], 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
advanced_filter: { advanced_filter: {
path: '/api/v2/advanced-filters', path: '/api/v2/advanced-filters',
...@@ -224,6 +242,9 @@ export const GENERAL_API_MISC_RESOURCES = { ...@@ -224,6 +242,9 @@ export const GENERAL_API_MISC_RESOURCES = {
config_csv_export: { config_csv_export: {
path: '/api/v2/config/csv-export', path: '/api/v2/config/csv-export',
}, },
config_celo: {
path: '/api/v2/config/celo',
},
// CSV EXPORT // CSV EXPORT
csv_export_token_holders: { csv_export_token_holders: {
...@@ -261,6 +282,7 @@ R extends 'general:search' ? SearchResult : ...@@ -261,6 +282,7 @@ R extends 'general:search' ? SearchResult :
R extends 'general:search_check_redirect' ? SearchRedirectResult : R extends 'general:search_check_redirect' ? SearchRedirectResult :
R extends 'general:config_backend_version' ? BackendVersionConfig : R extends 'general:config_backend_version' ? BackendVersionConfig :
R extends 'general:config_csv_export' ? CsvExportConfig : R extends 'general:config_csv_export' ? CsvExportConfig :
R extends 'general:config_celo' ? CeloConfig :
R extends 'general:blob' ? Blob : R extends 'general:blob' ? Blob :
R extends 'general:validators_stability' ? ValidatorsStabilityResponse : R extends 'general:validators_stability' ? ValidatorsStabilityResponse :
R extends 'general:validators_stability_counters' ? ValidatorsStabilityCountersResponse : R extends 'general:validators_stability_counters' ? ValidatorsStabilityCountersResponse :
...@@ -268,6 +290,9 @@ R extends 'general:validators_blackfort' ? ValidatorsBlackfortResponse : ...@@ -268,6 +290,9 @@ R extends 'general:validators_blackfort' ? ValidatorsBlackfortResponse :
R extends 'general:validators_blackfort_counters' ? ValidatorsBlackfortCountersResponse : R extends 'general:validators_blackfort_counters' ? ValidatorsBlackfortCountersResponse :
R extends 'general:validators_zilliqa' ? ValidatorsZilliqaResponse : R extends 'general:validators_zilliqa' ? ValidatorsZilliqaResponse :
R extends 'general:validator_zilliqa' ? ValidatorZilliqa : 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_ops' ? UserOpsResponse :
R extends 'general:user_op' ? UserOp : R extends 'general:user_op' ? UserOp :
R extends 'general:user_ops_account' ? UserOpsAccount : R extends 'general:user_ops_account' ? UserOpsAccount :
......
...@@ -118,6 +118,12 @@ export default function useNavItems(): ReturnType { ...@@ -118,6 +118,12 @@ export default function useNavItems(): ReturnType {
icon: 'MUD_menu', icon: 'MUD_menu',
isActive: pathname === '/mud-worlds', isActive: pathname === '/mud-worlds',
} : null; } : 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; const rollupFeature = config.features.rollup;
...@@ -196,6 +202,7 @@ export default function useNavItems(): ReturnType { ...@@ -196,6 +202,7 @@ export default function useNavItems(): ReturnType {
internalTxs, internalTxs,
userOps, userOps,
blocks, blocks,
epochs,
topAccounts, topAccounts,
validators, validators,
verifiedContracts, verifiedContracts,
......
...@@ -54,6 +54,8 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -54,6 +54,8 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/name-domains/[name]': 'Regular page', '/name-domains/[name]': 'Regular page',
'/validators': 'Root page', '/validators': 'Root page',
'/validators/[id]': 'Regular page', '/validators/[id]': 'Regular page',
'/epochs': 'Root page',
'/epochs/[number]': 'Regular page',
'/gas-tracker': 'Root page', '/gas-tracker': 'Root page',
'/mud-worlds': 'Root page', '/mud-worlds': 'Root page',
'/token-transfers': 'Root page', '/token-transfers': 'Root page',
......
...@@ -57,6 +57,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -57,6 +57,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/name-domains/[name]': DEFAULT_TEMPLATE, '/name-domains/[name]': DEFAULT_TEMPLATE,
'/validators': DEFAULT_TEMPLATE, '/validators': DEFAULT_TEMPLATE,
'/validators/[id]': 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.', '/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, '/mud-worlds': DEFAULT_TEMPLATE,
'/token-transfers': DEFAULT_TEMPLATE, '/token-transfers': DEFAULT_TEMPLATE,
......
...@@ -54,6 +54,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -54,6 +54,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/name-domains/[name]': '%network_name% %name% domain details', '/name-domains/[name]': '%network_name% %name% domain details',
'/validators': '%network_name% validators list', '/validators': '%network_name% validators list',
'/validators/[id]': '%network_name% validator %id% details', '/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%', '/gas-tracker': 'Track %network_name% gas fees in %network_gwei%',
'/mud-worlds': '%network_name% MUD worlds list', '/mud-worlds': '%network_name% MUD worlds list',
'/token-transfers': '%network_name% token transfers', '/token-transfers': '%network_name% token transfers',
......
...@@ -52,6 +52,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -52,6 +52,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/name-domains/[name]': 'Domain details', '/name-domains/[name]': 'Domain details',
'/validators': 'Validators list', '/validators': 'Validators list',
'/validators/[id]': 'Validator details', '/validators/[id]': 'Validator details',
'/epochs': 'Epochs',
'/epochs/[number]': 'Epoch details',
'/gas-tracker': 'Gas tracker', '/gas-tracker': 'Gas tracker',
'/mud-worlds': 'MUD worlds', '/mud-worlds': 'MUD worlds',
'/token-transfers': 'Token transfers', '/token-transfers': 'Token transfers',
......
...@@ -7,5 +7,7 @@ export default function shortenString(string: string | null, charNumber: number ...@@ -7,5 +7,7 @@ export default function shortenString(string: string | null, charNumber: number
return string; 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 = { ...@@ -11,8 +11,6 @@ export const epochRewards: AddressEpochRewardsResponse = {
amount: '136609473658452408568', amount: '136609473658452408568',
account: withName, account: withName,
associated_account: withName, associated_account: withName,
block_hash: '0x',
block_number: 26369280,
block_timestamp: '2022-05-15T13:16:24Z', block_timestamp: '2022-05-15T13:16:24Z',
epoch_number: 1526, epoch_number: 1526,
token: tokenInfo, token: tokenInfo,
...@@ -22,8 +20,6 @@ export const epochRewards: AddressEpochRewardsResponse = { ...@@ -22,8 +20,6 @@ export const epochRewards: AddressEpochRewardsResponse = {
amount: '117205842355246195095', amount: '117205842355246195095',
account: withoutName, account: withoutName,
associated_account: withoutName, associated_account: withoutName,
block_hash: '0x',
block_number: 26352000,
block_timestamp: '2022-05-15T13:16:24Z', block_timestamp: '2022-05-15T13:16:24Z',
epoch_number: 1525, epoch_number: 1525,
token: tokenInfo, token: tokenInfo,
...@@ -33,8 +29,6 @@ export const epochRewards: AddressEpochRewardsResponse = { ...@@ -33,8 +29,6 @@ export const epochRewards: AddressEpochRewardsResponse = {
amount: '125659647325556554060', amount: '125659647325556554060',
account: withEns, account: withEns,
associated_account: withEns, associated_account: withEns,
block_hash: '0x',
block_number: 26300160,
block_timestamp: '2022-05-15T13:16:24Z', block_timestamp: '2022-05-15T13:16:24Z',
epoch_number: 1524, epoch_number: 1524,
token: tokenInfo, token: tokenInfo,
...@@ -43,7 +37,7 @@ export const epochRewards: AddressEpochRewardsResponse = { ...@@ -43,7 +37,7 @@ export const epochRewards: AddressEpochRewardsResponse = {
next_page_params: { next_page_params: {
amount: '71952055594478242556', amount: '71952055594478242556',
associated_account_address_hash: '0x30d060f129817c4de5fbc1366d53e19f43c8c64f', associated_account_address_hash: '0x30d060f129817c4de5fbc1366d53e19f43c8c64f',
block_number: 25954560, epoch_number: 25954560,
items_count: 50, items_count: 50,
type: 'delegated_payment', type: 'delegated_payment',
}, },
......
...@@ -168,7 +168,7 @@ export const celo: Block = { ...@@ -168,7 +168,7 @@ export const celo: Block = {
recipient: addressMock.contract, recipient: addressMock.contract,
}, },
epoch_number: 1486, epoch_number: 1486,
is_epoch_block: true, l1_era_finalized_epoch_number: 1485,
}, },
}; };
......
import { padStart } from 'es-toolkit/compat'; import { padStart } from 'es-toolkit/compat';
import type { BlockEpoch, BlockEpochElectionRewardDetails, BlockEpochElectionRewardDetailsResponse } from 'types/api/block'; import type { CeloEpochDetails, CeloEpochElectionRewardDetails, CeloEpochElectionRewardDetailsResponse, CeloEpochListResponse } from 'types/api/epochs';
import * as addressMock from '../address/address'; import * as addressMock from '../address/address';
import * as tokenMock from '../tokens/tokenInfo'; import * as tokenMock from '../tokens/tokenInfo';
import * as tokenTransferMock from '../tokens/tokenTransfer'; import * as tokenTransferMock from '../tokens/tokenTransfer';
export const blockEpoch1: BlockEpoch = { export const epoch1: CeloEpochDetails = {
number: 1486, 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: { distribution: {
carbon_offsetting_transfer: tokenTransferMock.erc20, carbon_offsetting_transfer: tokenTransferMock.erc20,
community_transfer: tokenTransferMock.erc20, community_transfer: tokenTransferMock.erc20,
reserve_bolster_transfer: null, transfers_total: {
token: tokenMock.tokenInfoERC20a,
total: {
value: '1000000000000000000',
decimals: '18',
},
},
}, },
aggregated_election_rewards: { aggregated_election_rewards: {
delegated_payment: { delegated_payment: {
...@@ -37,7 +52,59 @@ export const blockEpoch1: BlockEpoch = { ...@@ -37,7 +52,59 @@ export const blockEpoch1: BlockEpoch = {
}, },
}; };
function getRewardDetailsItem(index: number): BlockEpochElectionRewardDetails { 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 { return {
amount: `${ 100 - index }210001063118670575`, amount: `${ 100 - index }210001063118670575`,
account: { account: {
...@@ -51,7 +118,7 @@ function getRewardDetailsItem(index: number): BlockEpochElectionRewardDetails { ...@@ -51,7 +118,7 @@ function getRewardDetailsItem(index: number): BlockEpochElectionRewardDetails {
}; };
} }
export const electionRewardDetails1: BlockEpochElectionRewardDetailsResponse = { export const electionRewardDetails1: CeloEpochElectionRewardDetailsResponse = {
items: Array(15).fill('').map((item, index) => getRewardDetailsItem(index)), items: Array(15).fill('').map((item, index) => getRewardDetailsItem(index)),
next_page_params: null, next_page_params: null,
}; };
...@@ -390,6 +390,16 @@ export const tac: GetServerSideProps<Props> = async(context) => { ...@@ -390,6 +390,16 @@ export const tac: GetServerSideProps<Props> = async(context) => {
return base(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) => { export const interopMessages: GetServerSideProps<Props> = async(context) => {
const rollupFeature = config.features.rollup; const rollupFeature = config.features.rollup;
if (!rollupFeature.isEnabled || !rollupFeature.interopEnabled) { if (!rollupFeature.isEnabled || !rollupFeature.interopEnabled) {
......
...@@ -43,6 +43,8 @@ declare module "nextjs-routes" { ...@@ -43,6 +43,8 @@ declare module "nextjs-routes" {
| StaticRoute<"/csv-export"> | StaticRoute<"/csv-export">
| StaticRoute<"/deposits"> | StaticRoute<"/deposits">
| StaticRoute<"/dispute-games"> | StaticRoute<"/dispute-games">
| DynamicRoute<"/epochs/[number]", { "number": string }>
| StaticRoute<"/epochs">
| StaticRoute<"/gas-tracker"> | StaticRoute<"/gas-tracker">
| StaticRoute<"/graphiql"> | StaticRoute<"/graphiql">
| StaticRoute<"/"> | StaticRoute<"/">
......
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';
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]>> = { ...@@ -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_OPERATION_LIFECYCLE_API_HOST', 'http://localhost:3100' ],
[ 'NEXT_PUBLIC_TAC_TON_EXPLORER_URL', 'https://testnet.tonviewer.com' ], [ 'NEXT_PUBLIC_TAC_TON_EXPLORER_URL', 'https://testnet.tonviewer.com' ],
], ],
celo: [
[ 'NEXT_PUBLIC_CELO_ENABLED', 'true' ],
],
}; };
...@@ -83,6 +83,7 @@ ...@@ -83,6 +83,7 @@
| "graphQL" | "graphQL"
| "heart_filled" | "heart_filled"
| "heart_outline" | "heart_outline"
| "hourglass_slim"
| "hourglass" | "hourglass"
| "info_filled" | "info_filled"
| "info" | "info"
......
...@@ -119,11 +119,9 @@ export const ADDRESS_MUD_TABLE_ITEM: AddressMudTableItem = { ...@@ -119,11 +119,9 @@ export const ADDRESS_MUD_TABLE_ITEM: AddressMudTableItem = {
export const EPOCH_REWARD_ITEM: AddressEpochRewardsItem = { export const EPOCH_REWARD_ITEM: AddressEpochRewardsItem = {
amount: '136609473658452408568', amount: '136609473658452408568',
block_number: 10355938,
block_timestamp: '2022-05-15T13:16:24Z', block_timestamp: '2022-05-15T13:16:24Z',
type: 'voter', type: 'voter',
token: TOKEN_INFO_ERC_20, token: TOKEN_INFO_ERC_20,
block_hash: '0x5956a847d8089e254e02e5111cad6992b99ceb9e5c2dc4343fd53002834c4dc6',
account: ADDRESS_PARAMS, account: ADDRESS_PARAMS,
epoch_number: 1234, epoch_number: 1234,
associated_account: ADDRESS_PARAMS, associated_account: ADDRESS_PARAMS,
......
import type { Block, BlockEpochElectionReward, BlockEpoch } from 'types/api/block'; import type { Block } from 'types/api/block';
import { ADDRESS_PARAMS } from './addressParams'; import { ADDRESS_PARAMS } from './addressParams';
import { TOKEN_INFO_ERC_20, TOKEN_TRANSFER_ERC_20 } from './token';
export const BLOCK_HASH = '0x8fa7b9e5e5e79deeb62d608db22ba9a5cb45388c7ebb9223ae77331c6080dc70'; export const BLOCK_HASH = '0x8fa7b9e5e5e79deeb62d608db22ba9a5cb45388c7ebb9223ae77331c6080dc70';
...@@ -37,24 +36,3 @@ export const BLOCK: Block = { ...@@ -37,24 +36,3 @@ export const BLOCK: Block = {
type: 'block', type: 'block',
uncles_hashes: [], 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,
},
};
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 { ...@@ -8,7 +8,7 @@ import type {
TokenType, TokenType,
} from 'types/api/token'; } from 'types/api/token';
import type { TokenInstanceTransferPagination, TokenInstanceTransferResponse } from 'types/api/tokens'; 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 { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
import { TX_HASH } from './tx'; import { TX_HASH } from './tx';
...@@ -89,6 +89,11 @@ export const getTokenInstanceHoldersStub = (type?: TokenType, pagination: TokenH ...@@ -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 = { export const TOKEN_TRANSFER_ERC_20: TokenTransfer = {
block_hash: BLOCK_HASH, block_hash: BLOCK_HASH,
block_number: '123456', block_number: '123456',
...@@ -98,10 +103,7 @@ export const TOKEN_TRANSFER_ERC_20: TokenTransfer = { ...@@ -98,10 +103,7 @@ export const TOKEN_TRANSFER_ERC_20: TokenTransfer = {
timestamp: '2022-06-24T10:22:11.000000Z', timestamp: '2022-06-24T10:22:11.000000Z',
to: ADDRESS_PARAMS, to: ADDRESS_PARAMS,
token: TOKEN_INFO_ERC_20, token: TOKEN_INFO_ERC_20,
total: { total: TOKEN_TRANSFER_ERC_20_TOTAL,
decimals: '18',
value: '9851351626684503',
},
transaction_hash: TX_HASH, transaction_hash: TX_HASH,
type: 'token_minting', type: 'token_minting',
}; };
......
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
import type { UserTags, AddressImplementation, AddressParam, AddressFilecoinParams } from './addressParams'; 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 { SmartContractProxyType } from './contract';
import type { CeloEpochRewardsType } from './epochs';
import type { InternalTransaction } from './internalTransaction'; import type { InternalTransaction } from './internalTransaction';
import type { MudWorldSchema, MudWorldTable } from './mudWorlds'; import type { MudWorldSchema, MudWorldTable } from './mudWorlds';
import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token'; import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token';
...@@ -260,18 +261,16 @@ export type AddressEpochRewardsResponse = { ...@@ -260,18 +261,16 @@ export type AddressEpochRewardsResponse = {
next_page_params: { next_page_params: {
amount: string; amount: string;
associated_account_address_hash: string; associated_account_address_hash: string;
block_number: number; epoch_number: number;
items_count: number; items_count: number;
type: EpochRewardsType; type: CeloEpochRewardsType;
} | null; } | null;
}; };
export type AddressEpochRewardsItem = { export type AddressEpochRewardsItem = {
type: EpochRewardsType; type: CeloEpochRewardsType;
token: TokenInfo; token: TokenInfo;
amount: string; amount: string;
block_number: number;
block_hash: string;
block_timestamp: string; block_timestamp: string;
account: AddressParam; account: AddressParam;
epoch_number: number; epoch_number: number;
......
...@@ -6,7 +6,6 @@ import type { ArbitrumBatchStatus, ArbitrumL2TxData } from './arbitrumL2'; ...@@ -6,7 +6,6 @@ import type { ArbitrumBatchStatus, ArbitrumL2TxData } from './arbitrumL2';
import type { InternalTransaction } from './internalTransaction'; import type { InternalTransaction } from './internalTransaction';
import type { OptimisticL2BatchDataContainer, OptimisticL2BlobTypeEip4844, OptimisticL2BlobTypeCelestia } from './optimisticL2'; import type { OptimisticL2BatchDataContainer, OptimisticL2BlobTypeEip4844, OptimisticL2BlobTypeCelestia } from './optimisticL2';
import type { TokenInfo } from './token'; import type { TokenInfo } from './token';
import type { TokenTransfer } from './tokenTransfer';
import type { ZkSyncBatchesItem } from './zkSyncL2'; import type { ZkSyncBatchesItem } from './zkSyncL2';
export type BlockType = 'block' | 'reorg' | 'uncle'; export type BlockType = 'block' | 'reorg' | 'uncle';
...@@ -66,7 +65,7 @@ export interface Block { ...@@ -66,7 +65,7 @@ export interface Block {
// CELO FIELDS // CELO FIELDS
celo?: { celo?: {
epoch_number: number; epoch_number: number;
is_epoch_block: boolean; l1_era_finalized_epoch_number: number | null;
base_fee?: BlockBaseFeeCelo; base_fee?: BlockBaseFeeCelo;
}; };
// ZILLIQA FIELDS // ZILLIQA FIELDS
...@@ -167,32 +166,3 @@ export interface BlockCountdownResponse { ...@@ -167,32 +166,3 @@ export interface BlockCountdownResponse {
RemainingBlock: string; RemainingBlock: string;
} | null; } | 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 { ...@@ -5,3 +5,7 @@ export interface BackendVersionConfig {
export interface CsvExportConfig { export interface CsvExportConfig {
limit: number; limit: number;
} }
export interface CeloConfig {
l2_migration_block: number;
}
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'; ...@@ -12,7 +12,7 @@ import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import AddressCsvExportLink from './AddressCsvExportLink'; // import AddressCsvExportLink from './AddressCsvExportLink';
import AddressEpochRewardsListItem from './epochRewards/AddressEpochRewardsListItem'; import AddressEpochRewardsListItem from './epochRewards/AddressEpochRewardsListItem';
type Props = { type Props = {
...@@ -38,7 +38,7 @@ const AddressEpochRewards = ({ shouldRender = true, isQueryEnabled = true }: Pro ...@@ -38,7 +38,7 @@ const AddressEpochRewards = ({ shouldRender = true, isQueryEnabled = true }: Pro
items_count: 50, items_count: 50,
type: 'voter', type: 'voter',
associated_account_address_hash: '1', associated_account_address_hash: '1',
block_number: 10355938, epoch_number: 10355938,
} }), } }),
}, },
}); });
...@@ -59,7 +59,7 @@ const AddressEpochRewards = ({ shouldRender = true, isQueryEnabled = true }: Pro ...@@ -59,7 +59,7 @@ const AddressEpochRewards = ({ shouldRender = true, isQueryEnabled = true }: Pro
<Box hideFrom="lg"> <Box hideFrom="lg">
{ rewardsQuery.data.items.map((item, index) => ( { rewardsQuery.data.items.map((item, index) => (
<AddressEpochRewardsListItem <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 } item={ item }
isLoading={ rewardsQuery.isPlaceholderData } isLoading={ rewardsQuery.isPlaceholderData }
/> />
...@@ -70,13 +70,17 @@ const AddressEpochRewards = ({ shouldRender = true, isQueryEnabled = true }: Pro ...@@ -70,13 +70,17 @@ const AddressEpochRewards = ({ shouldRender = true, isQueryEnabled = true }: Pro
const actionBar = rewardsQuery.pagination.isVisible ? ( const actionBar = rewardsQuery.pagination.isVisible ? (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
<AddressCsvExportLink { /* <AddressCsvExportLink
address={ hash } address={ hash }
isLoading={ rewardsQuery.pagination.isLoading } isLoading={ rewardsQuery.pagination.isLoading }
params={{ type: 'epoch-rewards' }} params={{ type: 'epoch-rewards' }}
ml={{ lg: 'auto' }} ml={{ lg: 'auto' }}
/> */ }
<Pagination
ml="auto"
// ml={{ base: 0, lg: 8 }}
{ ...rewardsQuery.pagination }
/> />
<Pagination ml={{ base: 0, lg: 8 }} { ...rewardsQuery.pagination }/>
</ActionBar> </ActionBar>
) : null; ) : null;
......
...@@ -5,7 +5,7 @@ import type { AddressEpochRewardsItem } from 'types/api/address'; ...@@ -5,7 +5,7 @@ import type { AddressEpochRewardsItem } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import { Skeleton } from 'toolkit/chakra/skeleton'; import { Skeleton } from 'toolkit/chakra/skeleton';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; 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 TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag'; import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
...@@ -21,20 +21,9 @@ const AddressEpochRewardsListItem = ({ item, isLoading }: Props) => { ...@@ -21,20 +21,9 @@ const AddressEpochRewardsListItem = ({ item, isLoading }: Props) => {
return ( return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto"> <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.Label isLoading={ isLoading }>Epoch #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<Skeleton loading={ isLoading }> <EpochEntity number={ String(item.epoch_number) } noIcon isLoading={ isLoading }/>
{ item.epoch_number }
</Skeleton>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
......
...@@ -19,7 +19,7 @@ const AddressEpochRewardsTable = ({ items, isLoading, top }: Props) => { ...@@ -19,7 +19,7 @@ const AddressEpochRewardsTable = ({ items, isLoading, top }: Props) => {
<TableHeaderSticky top={ top }> <TableHeaderSticky top={ top }>
<TableRow> <TableRow>
<TableColumnHeader> <TableColumnHeader>
Block Epoch
<TimeFormatToggle/> <TimeFormatToggle/>
</TableColumnHeader> </TableColumnHeader>
<TableColumnHeader>Reward type</TableColumnHeader> <TableColumnHeader>Reward type</TableColumnHeader>
...@@ -31,7 +31,7 @@ const AddressEpochRewardsTable = ({ items, isLoading, top }: Props) => { ...@@ -31,7 +31,7 @@ const AddressEpochRewardsTable = ({ items, isLoading, top }: Props) => {
{ items.map((item, index) => { { items.map((item, index) => {
return ( return (
<AddressEpochRewardsTableItem <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 } item={ item }
isLoading={ isLoading } isLoading={ isLoading }
/> />
......
import { Flex, Text } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressEpochRewardsItem } from 'types/api/address'; import type { AddressEpochRewardsItem } from 'types/api/address';
import { route } from 'nextjs-routes';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton'; import { Skeleton } from 'toolkit/chakra/skeleton';
import { TableCell, TableRow } from 'toolkit/chakra/table'; import { TableCell, TableRow } from 'toolkit/chakra/table';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; 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 TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag'; import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip'; import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip';
...@@ -23,10 +25,12 @@ const AddressEpochRewardsTableItem = ({ item, isLoading }: Props) => { ...@@ -23,10 +25,12 @@ const AddressEpochRewardsTableItem = ({ item, isLoading }: Props) => {
<TableRow> <TableRow>
<TableCell verticalAlign="middle"> <TableCell verticalAlign="middle">
<Flex alignItems="center" gap={ 3 }> <Flex alignItems="center" gap={ 3 }>
<BlockEntity number={ item.block_number } isLoading={ isLoading } noIcon fontWeight={ 600 }/> <Link
<Skeleton loading={ isLoading }> href={ route({ pathname: '/epochs/[number]', query: { number: String(item.epoch_number) } }) }
<Text color="text.secondary" fontWeight={ 600 }>{ `Epoch # ${ item.epoch_number }` }</Text> loading={ isLoading }
</Skeleton> >
{ item.epoch_number }
</Link>
<TimeWithTooltip timestamp={ item.block_timestamp } isLoading={ isLoading } color="text.secondary" fontWeight={ 400 }/> <TimeWithTooltip timestamp={ item.block_timestamp } isLoading={ isLoading } color="text.secondary" fontWeight={ 400 }/>
</Flex> </Flex>
</TableCell> </TableCell>
......
import { HStack } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import config from 'configs/app';
import { Link } from 'toolkit/chakra/link'; import { Link } from 'toolkit/chakra/link';
import { Tag } from 'toolkit/chakra/tag'; import { Tag } from 'toolkit/chakra/tag';
import { Tooltip } from 'toolkit/chakra/tooltip'; import { Tooltip } from 'toolkit/chakra/tooltip';
...@@ -13,39 +13,44 @@ interface Props { ...@@ -13,39 +13,44 @@ interface Props {
blockQuery: BlockQuery; blockQuery: BlockQuery;
} }
const BlockCeloEpochTag = ({ blockQuery }: Props) => { const BlockCeloEpochTagRegular = ({ blockQuery }: Props) => {
if (!blockQuery.data?.celo) { if (!blockQuery.data?.celo) {
return null; return null;
} }
if (!blockQuery.data.celo.is_epoch_block) { return (
const celoConfig = config.features.celo; <Tooltip
const epochBlockNumber = celoConfig.isEnabled && celoConfig.L2UpgradeBlock && blockQuery.data.height <= celoConfig.L2UpgradeBlock ? key="epoch-tag-before-finalized"
blockQuery.data.celo.epoch_number * celoConfig.BLOCKS_PER_EPOCH : content="Displays the epoch this block belongs to before the epoch is finalized"
undefined; >
const content = epochBlockNumber ? ( <Link href={ route({ pathname: '/epochs/[number]', query: { number: String(blockQuery.data.celo.epoch_number) } }) }>
<Link href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: String(epochBlockNumber) } }) }>
<Tag variant="clickable">Epoch #{ blockQuery.data.celo.epoch_number }</Tag> <Tag variant="clickable">Epoch #{ blockQuery.data.celo.epoch_number }</Tag>
</Link> </Link>
) : <Tag>Epoch #{ blockQuery.data.celo.epoch_number }</Tag>; </Tooltip>
);
};
return ( const BlockCeloEpochTag = ({ blockQuery }: Props) => {
<Tooltip if (!blockQuery.data?.celo) {
key="epoch-tag-before-finalized" return null;
content="Displays the epoch this block belongs to before the epoch is finalized" }
>
{ content } if (!blockQuery.data.celo.l1_era_finalized_epoch_number) {
</Tooltip> return <BlockCeloEpochTagRegular blockQuery={ blockQuery }/>;
);
} }
return ( return (
<Tooltip <HStack gap={ 2 }>
key="epoch-tag" <Tooltip
content="Displays the epoch finalized by this block" 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> <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>
); );
}; };
......
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);
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);
...@@ -115,7 +115,7 @@ const BlocksContent = ({ type, query, enableSocket = true, top }: Props) => { ...@@ -115,7 +115,7 @@ const BlocksContent = ({ type, query, enableSocket = true, top }: Props) => {
const actionBar = isMobile ? ( const actionBar = isMobile ? (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
<Link href={ route({ pathname: '/block/countdown' }) }> <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> <span>Block countdown</span>
</Link> </Link>
<Pagination ml="auto" { ...query.pagination }/> <Pagination ml="auto" { ...query.pagination }/>
......
...@@ -51,8 +51,8 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement, animation }: Pro ...@@ -51,8 +51,8 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement, animation }: Pro
noIcon noIcon
fontWeight={ 600 } fontWeight={ 600 }
/> />
{ data.celo?.is_epoch_block && ( { data.celo?.l1_era_finalized_epoch_number && (
<Tooltip content={ `Finalized epoch #${ data.celo.epoch_number }` } disabled={ isLoading }> <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 }/> <IconSvg name="checkered_flag" boxSize={ 5 } p="1px" isLoading={ isLoading } flexShrink={ 0 }/>
</Tooltip> </Tooltip>
) } ) }
......
...@@ -37,7 +37,7 @@ const BlocksTabSlot = ({ pagination }: Props) => { ...@@ -37,7 +37,7 @@ const BlocksTabSlot = ({ pagination }: Props) => {
</Box> </Box>
) } ) }
<Link href={ route({ pathname: '/block/countdown' }) }> <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> <span>Block countdown</span>
</Link> </Link>
<Pagination my={ 1 } { ...pagination }/> <Pagination my={ 1 } { ...pagination }/>
......
...@@ -41,8 +41,8 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement, animation }: Pr ...@@ -41,8 +41,8 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement, animation }: Pr
<TableRow animation={ animation }> <TableRow animation={ animation }>
<TableCell > <TableCell >
<Flex columnGap={ 2 } alignItems="center" mb={ 2 }> <Flex columnGap={ 2 } alignItems="center" mb={ 2 }>
{ data.celo?.is_epoch_block && ( { data.celo?.l1_era_finalized_epoch_number && (
<Tooltip content={ `Finalized epoch #${ data.celo.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 }/> <IconSvg name="checkered_flag" boxSize={ 5 } p="1px" isLoading={ isLoading } flexShrink={ 0 }/>
</Tooltip> </Tooltip>
) } ) }
......
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);
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;
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;
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'; ...@@ -2,7 +2,7 @@ import { Box, Grid, GridItem, Text } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; 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 type { TokenInfo } from 'types/api/token';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
...@@ -14,20 +14,20 @@ import useLazyLoadedList from 'ui/shared/pagination/useLazyLoadedList'; ...@@ -14,20 +14,20 @@ import useLazyLoadedList from 'ui/shared/pagination/useLazyLoadedList';
import { formatRewardType, getRewardDetailsTableTitles } from './utils'; import { formatRewardType, getRewardDetailsTableTitles } from './utils';
interface Props { interface Props {
type: keyof BlockEpoch['aggregated_election_rewards']; type: keyof CeloEpochDetails['aggregated_election_rewards'];
token: TokenInfo; token: TokenInfo;
} }
const BlockEpochElectionRewardDetailsDesktop = ({ type, token }: Props) => { const CeloEpochElectionRewardDetailsDesktop = ({ type, token }: Props) => {
const rootRef = React.useRef<HTMLDivElement>(null); const rootRef = React.useRef<HTMLDivElement>(null);
const router = useRouter(); const router = useRouter();
const heightOrHash = getQueryParamString(router.query.height_or_hash); const number = getQueryParamString(router.query.number);
const { cutRef, query } = useLazyLoadedList({ const { cutRef, query } = useLazyLoadedList({
rootRef, rootRef,
resourceName: 'general:block_election_rewards', resourceName: 'general:epoch_celo_election_rewards',
pathParams: { height_or_hash: heightOrHash, reward_type: formatRewardType(type) }, pathParams: { number: number, reward_type: formatRewardType(type) },
queryOptions: { queryOptions: {
refetchOnMount: false, refetchOnMount: false,
}, },
...@@ -97,4 +97,4 @@ const BlockEpochElectionRewardDetailsDesktop = ({ type, token }: Props) => { ...@@ -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'; ...@@ -2,7 +2,7 @@ import { Box, Flex, Text } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; 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 type { TokenInfo } from 'types/api/token';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
...@@ -15,20 +15,20 @@ import useLazyLoadedList from 'ui/shared/pagination/useLazyLoadedList'; ...@@ -15,20 +15,20 @@ import useLazyLoadedList from 'ui/shared/pagination/useLazyLoadedList';
import { formatRewardType } from './utils'; import { formatRewardType } from './utils';
interface Props { interface Props {
type: keyof BlockEpoch['aggregated_election_rewards']; type: keyof CeloEpochDetails['aggregated_election_rewards'];
token: TokenInfo; token: TokenInfo;
} }
const BlockEpochElectionRewardDetailsMobile = ({ type, token }: Props) => { const CeloEpochElectionRewardDetailsMobile = ({ type, token }: Props) => {
const rootRef = React.useRef<HTMLDivElement>(null); const rootRef = React.useRef<HTMLDivElement>(null);
const router = useRouter(); const router = useRouter();
const heightOrHash = getQueryParamString(router.query.height_or_hash); const number = getQueryParamString(router.query.number);
const { cutRef, query } = useLazyLoadedList({ const { cutRef, query } = useLazyLoadedList({
rootRef, rootRef,
resourceName: 'general:block_election_rewards', resourceName: 'general:epoch_celo_election_rewards',
pathParams: { height_or_hash: heightOrHash, reward_type: formatRewardType(type) }, pathParams: { number: number, reward_type: formatRewardType(type) },
queryOptions: { queryOptions: {
refetchOnMount: false, refetchOnMount: false,
}, },
...@@ -80,4 +80,4 @@ const BlockEpochElectionRewardDetailsMobile = ({ type, token }: Props) => { ...@@ -80,4 +80,4 @@ const BlockEpochElectionRewardDetailsMobile = ({ type, token }: Props) => {
); );
}; };
export default React.memo(BlockEpochElectionRewardDetailsMobile); export default React.memo(CeloEpochElectionRewardDetailsMobile);
import React from 'react'; import React from 'react';
import * as blockEpochMock from 'mocks/blocks/epoch'; import * as celoEpochMock from 'mocks/epochs/celo';
import { test, expect } from 'playwright/lib'; import { test, expect } from 'playwright/lib';
import BlockEpochElectionRewards from './BlockEpochElectionRewards'; import EpochElectionRewards from './EpochElectionRewards';
const heightOrHash = '1234'; const number = '1234';
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { height_or_hash: heightOrHash }, query: { number },
}, },
}; };
test('base view', async({ render, mockApiResponse }) => { test('base view', async({ render, mockApiResponse }) => {
await mockApiResponse( await mockApiResponse(
'general:block_election_rewards', 'general:epoch_celo_election_rewards',
blockEpochMock.electionRewardDetails1, celoEpochMock.electionRewardDetails1,
{ pathParams: { height_or_hash: heightOrHash, reward_type: 'voter' } }, { pathParams: { number, reward_type: 'voter' } },
); );
const component = await render(<BlockEpochElectionRewards data={ blockEpochMock.blockEpoch1 }/>, { hooksConfig }); const component = await render(<EpochElectionRewards data={ celoEpochMock.epoch1 }/>, { hooksConfig });
await component.getByRole('cell', { name: 'Voting rewards' }).click(); await component.getByRole('cell', { name: 'Voting rewards' }).click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('base view +@mobile -@default', async({ render, mockApiResponse }) => { test('base view +@mobile -@default', async({ render, mockApiResponse }) => {
await mockApiResponse( await mockApiResponse(
'general:block_election_rewards', 'general:epoch_celo_election_rewards',
blockEpochMock.electionRewardDetails1, celoEpochMock.electionRewardDetails1,
{ pathParams: { height_or_hash: heightOrHash, reward_type: 'voter' } }, { pathParams: { number, reward_type: 'voter' } },
); );
const component = await render(<BlockEpochElectionRewards data={ blockEpochMock.blockEpoch1 }/>, { hooksConfig }); const component = await render(<EpochElectionRewards data={ celoEpochMock.epoch1 }/>, { hooksConfig });
await component.locator('div').filter({ hasText: 'Voting rewards' }).nth(3).click(); await component.locator('div').filter({ hasText: 'Voting rewards' }).nth(3).click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import React from '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 { Heading } from 'toolkit/chakra/heading';
import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table'; import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table';
import BlockEpochElectionRewardsListItem from './BlockEpochElectionRewardsListItem'; import EpochElectionRewardsListItem from './EpochElectionRewardsListItem';
import BlockEpochElectionRewardsTableItem from './BlockEpochElectionRewardsTableItem'; import EpochElectionRewardsTableItem from './EpochElectionRewardsTableItem';
interface Props { interface Props {
data: BlockEpoch; data: CeloEpochDetails;
isLoading?: boolean; isLoading?: boolean;
} }
const BlockEpochElectionRewards = ({ data, isLoading }: Props) => { const EpochElectionRewards = ({ data, isLoading }: Props) => {
if (!data.aggregated_election_rewards) { if (!data.aggregated_election_rewards) {
return null; return null;
} }
return ( return (
<Box mt={ 8 }> <Box mt={ 6 }>
<Heading level="3" mb={ 3 }>Election rewards</Heading> <Heading level="3" mb={ 3 }>Election rewards</Heading>
<Box hideBelow="lg"> <Box hideBelow="lg">
<TableRoot style={{ tableLayout: 'auto' }}> <TableRoot style={{ tableLayout: 'auto' }}>
...@@ -34,7 +34,7 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => { ...@@ -34,7 +34,7 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => {
</TableHeaderSticky> </TableHeaderSticky>
<TableBody> <TableBody>
{ Object.entries(data.aggregated_election_rewards).map((entry) => { { 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]; const value = entry[1];
if (!value) { if (!value) {
...@@ -42,7 +42,7 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => { ...@@ -42,7 +42,7 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => {
} }
return ( return (
<BlockEpochElectionRewardsTableItem <EpochElectionRewardsTableItem
key={ key } key={ key }
type={ key } type={ key }
isLoading={ isLoading } isLoading={ isLoading }
...@@ -55,7 +55,7 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => { ...@@ -55,7 +55,7 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => {
</Box> </Box>
<Box hideFrom="lg"> <Box hideFrom="lg">
{ Object.entries(data.aggregated_election_rewards).map((entry) => { { 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]; const value = entry[1];
if (!value) { if (!value) {
...@@ -63,7 +63,7 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => { ...@@ -63,7 +63,7 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => {
} }
return ( return (
<BlockEpochElectionRewardsListItem <EpochElectionRewardsListItem
key={ key } key={ key }
type={ key } type={ key }
isLoading={ isLoading } isLoading={ isLoading }
...@@ -76,4 +76,4 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => { ...@@ -76,4 +76,4 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => {
); );
}; };
export default React.memo(BlockEpochElectionRewards); export default React.memo(EpochElectionRewards);
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import React from '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 getCurrencyValue from 'lib/getCurrencyValue';
import { IconButton } from 'toolkit/chakra/icon-button'; import { IconButton } from 'toolkit/chakra/icon-button';
...@@ -11,15 +11,16 @@ import TokenEntity from 'ui/shared/entities/token/TokenEntity'; ...@@ -11,15 +11,16 @@ import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag'; import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import BlockEpochElectionRewardDetailsMobile from './BlockEpochElectionRewardDetailsMobile'; import EpochElectionRewardDetailsMobile from './EpochElectionRewardDetailsMobile';
import { getRewardNumText } from './utils';
interface Props { interface Props {
data: BlockEpochElectionReward; data: CeloEpochElectionReward;
type: keyof BlockEpoch['aggregated_election_rewards']; type: keyof CeloEpochDetails['aggregated_election_rewards'];
isLoading?: boolean; isLoading?: boolean;
} }
const BlockEpochElectionRewardsListItem = ({ data, isLoading, type }: Props) => { const EpochElectionRewardsListItem = ({ data, isLoading, type }: Props) => {
const section = useDisclosure(); const section = useDisclosure();
const { valueStr } = getCurrencyValue({ const { valueStr } = getCurrencyValue({
...@@ -55,7 +56,7 @@ const BlockEpochElectionRewardsListItem = ({ data, isLoading, type }: Props) => ...@@ -55,7 +56,7 @@ const BlockEpochElectionRewardsListItem = ({ data, isLoading, type }: Props) =>
</Skeleton> </Skeleton>
) : <Box boxSize={ 6 }/> } ) : <Box boxSize={ 6 }/> }
<EpochRewardTypeTag type={ type } isLoading={ isLoading }/> <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 }> <Flex columnGap={ 2 } alignItems="center" ml={{ base: 9, lg: 'auto' }} w={{ base: '100%', lg: 'fit-content' }} fontWeight={ 500 }>
<Skeleton loading={ isLoading }>{ valueStr }</Skeleton> <Skeleton loading={ isLoading }>{ valueStr }</Skeleton>
<TokenEntity <TokenEntity
...@@ -69,11 +70,11 @@ const BlockEpochElectionRewardsListItem = ({ data, isLoading, type }: Props) => ...@@ -69,11 +70,11 @@ const BlockEpochElectionRewardsListItem = ({ data, isLoading, type }: Props) =>
</Flex> </Flex>
{ section.open && ( { section.open && (
<Box mt={ 2 }> <Box mt={ 2 }>
<BlockEpochElectionRewardDetailsMobile type={ type } token={ data.token }/> <EpochElectionRewardDetailsMobile type={ type } token={ data.token }/>
</Box> </Box>
) } ) }
</Box> </Box>
); );
}; };
export default React.memo(BlockEpochElectionRewardsListItem); export default React.memo(EpochElectionRewardsListItem);
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import React from '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 getCurrencyValue from 'lib/getCurrencyValue';
import { IconButton } from 'toolkit/chakra/icon-button'; import { IconButton } from 'toolkit/chakra/icon-button';
...@@ -12,16 +12,16 @@ import TokenEntity from 'ui/shared/entities/token/TokenEntity'; ...@@ -12,16 +12,16 @@ import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag'; import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import BlockEpochElectionRewardDetailsDesktop from './BlockEpochElectionRewardDetailsDesktop'; import EpochElectionRewardDetailsDesktop from './EpochElectionRewardDetailsDesktop';
import { getRewardNumText } from './utils'; import { getRewardNumText } from './utils';
interface Props { interface Props {
data: BlockEpochElectionReward; data: CeloEpochElectionReward;
type: keyof BlockEpoch['aggregated_election_rewards']; type: keyof CeloEpochDetails['aggregated_election_rewards'];
isLoading?: boolean; isLoading?: boolean;
} }
const BlockEpochElectionRewardsTableItem = ({ isLoading, data, type }: Props) => { const EpochElectionRewardsTableItem = ({ isLoading, data, type }: Props) => {
const section = useDisclosure(); const section = useDisclosure();
const { valueStr } = getCurrencyValue({ const { valueStr } = getCurrencyValue({
...@@ -79,7 +79,7 @@ const BlockEpochElectionRewardsTableItem = ({ isLoading, data, type }: Props) => ...@@ -79,7 +79,7 @@ const BlockEpochElectionRewardsTableItem = ({ isLoading, data, type }: Props) =>
<TableRow> <TableRow>
<TableCell/> <TableCell/>
<TableCell colSpan={ 3 } pr={ 0 } pt={ 0 }> <TableCell colSpan={ 3 } pr={ 0 } pt={ 0 }>
<BlockEpochElectionRewardDetailsDesktop type={ type } token={ data.token }/> <EpochElectionRewardDetailsDesktop type={ type } token={ data.token }/>
</TableCell> </TableCell>
</TableRow> </TableRow>
) } ) }
...@@ -87,4 +87,4 @@ const BlockEpochElectionRewardsTableItem = ({ isLoading, data, type }: Props) => ...@@ -87,4 +87,4 @@ const BlockEpochElectionRewardsTableItem = ({ isLoading, data, type }: Props) =>
); );
}; };
export default React.memo(BlockEpochElectionRewardsTableItem); export default React.memo(EpochElectionRewardsTableItem);
import type { BlockEpoch } from 'types/api/block'; import type { CeloEpochDetails } from 'types/api/epochs';
import type { ExcludeNull } from 'types/utils'; 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 postfix1 = num !== 1 ? 's' : '';
const postfix2 = num !== 1 ? 'es' : ''; const postfix2 = num !== 1 ? 'es' : '';
...@@ -27,7 +27,7 @@ export function getRewardNumText(type: keyof BlockEpoch['aggregated_election_rew ...@@ -27,7 +27,7 @@ export function getRewardNumText(type: keyof BlockEpoch['aggregated_election_rew
return `${ num } ${ text }`; 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) { switch (type) {
case 'delegated_payment': case 'delegated_payment':
return [ 'Beneficiary', 'Validator' ]; return [ 'Beneficiary', 'Validator' ];
...@@ -40,6 +40,6 @@ export function getRewardDetailsTableTitles(type: keyof ExcludeNull<BlockEpoch[' ...@@ -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('_', '-'); return type.replaceAll('_', '-');
} }
...@@ -38,8 +38,8 @@ const LatestBlocksItem = ({ block, isLoading, animation }: Props) => { ...@@ -38,8 +38,8 @@ const LatestBlocksItem = ({ block, isLoading, animation }: Props) => {
fontWeight={ 500 } fontWeight={ 500 }
mr="auto" mr="auto"
/> />
{ block.celo?.is_epoch_block && ( { block.celo?.l1_era_finalized_epoch_number && (
<Tooltip content={ `Finalized epoch #${ block.celo.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 }/> <IconSvg name="checkered_flag" boxSize={ 5 } p="1px" ml={ 2 } isLoading={ isLoading } flexShrink={ 0 }/>
</Tooltip> </Tooltip>
) } ) }
......
...@@ -200,9 +200,10 @@ const Stats = () => { ...@@ -200,9 +200,10 @@ const Stats = () => {
}, },
apiData?.celo && { apiData?.celo && {
id: 'current_epoch' as const, id: 'current_epoch' as const,
icon: 'hourglass' as const, icon: 'hourglass_slim' as const,
label: 'Current epoch', label: 'Current epoch',
value: `#${ apiData.celo.epoch_number }`, value: `#${ apiData.celo.epoch_number }`,
href: { pathname: '/epochs/[number]' as const, query: { number: String(apiData.celo.epoch_number) } },
isLoading, isLoading,
}, },
] ]
......
...@@ -17,7 +17,6 @@ import { Skeleton } from 'toolkit/chakra/skeleton'; ...@@ -17,7 +17,6 @@ import { Skeleton } from 'toolkit/chakra/skeleton';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs'; import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import BlockCeloEpochTag from 'ui/block/BlockCeloEpochTag'; import BlockCeloEpochTag from 'ui/block/BlockCeloEpochTag';
import BlockDetails from 'ui/block/BlockDetails'; import BlockDetails from 'ui/block/BlockDetails';
import BlockEpochRewards from 'ui/block/BlockEpochRewards';
import BlockInternalTxs from 'ui/block/BlockInternalTxs'; import BlockInternalTxs from 'ui/block/BlockInternalTxs';
import BlockWithdrawals from 'ui/block/BlockWithdrawals'; import BlockWithdrawals from 'ui/block/BlockWithdrawals';
import useBlockBlobTxsQuery from 'ui/block/useBlockBlobTxsQuery'; import useBlockBlobTxsQuery from 'ui/block/useBlockBlobTxsQuery';
...@@ -110,12 +109,7 @@ const BlockPageContent = () => { ...@@ -110,12 +109,7 @@ const BlockPageContent = () => {
</> </>
), ),
} : null, } : null,
blockQuery.data?.celo?.is_epoch_block ? { ].filter(Boolean)), [ blockBlobTxsQuery, blockInternalTxsQuery, blockQuery, blockTxsQuery, blockWithdrawalsQuery, hasPagination ]);
id: 'epoch_rewards',
title: 'Epoch rewards',
component: <BlockEpochRewards heightOrHash={ heightOrHash }/>,
} : null,
].filter(Boolean)), [ blockBlobTxsQuery, blockInternalTxsQuery, blockQuery, blockTxsQuery, blockWithdrawalsQuery, hasPagination, heightOrHash ]);
let pagination; let pagination;
if (tab === 'txs') { if (tab === 'txs') {
......
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();
});
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;
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();
});
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;
...@@ -10,16 +10,17 @@ type Props = { ...@@ -10,16 +10,17 @@ type Props = {
timestamp: string | number; timestamp: string | number;
isLoading?: boolean; isLoading?: boolean;
noIcon?: boolean; noIcon?: boolean;
gap?: number;
}; };
const DetailedInfoTimestamp = ({ timestamp, isLoading, noIcon }: Props) => { const DetailedInfoTimestamp = ({ timestamp, isLoading, noIcon, gap }: Props) => {
return ( return (
<> <>
{ !noIcon && <IconSvg name="clock" boxSize={ 5 } color="gray.500" isLoading={ isLoading } mr={ 2 }/> } { !noIcon && <IconSvg name="clock" boxSize={ 5 } color="gray.500" isLoading={ isLoading } mr={ 2 }/> }
<Skeleton loading={ isLoading }> <Skeleton loading={ isLoading }>
{ dayjs(timestamp).fromNow() } { dayjs(timestamp).fromNow() }
</Skeleton> </Skeleton>
<TextSeparator color="gray.500"/> <TextSeparator color="gray.500" mx={ gap ?? 3 }/>
<Skeleton loading={ isLoading } whiteSpace="normal"> <Skeleton loading={ isLoading } whiteSpace="normal">
{ dayjs(timestamp).format('llll') } { dayjs(timestamp).format('llll') }
</Skeleton> </Skeleton>
......
import React from 'react'; 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 type { BadgeProps } from 'toolkit/chakra/badge';
import { Badge } from 'toolkit/chakra/badge'; import { Badge } from 'toolkit/chakra/badge';
import { Tooltip } from 'toolkit/chakra/tooltip'; import { Tooltip } from 'toolkit/chakra/tooltip';
type Props = { type Props = {
type: EpochRewardsType; type: CeloEpochRewardsType;
isLoading?: boolean; 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: { group: {
text: 'Validator group rewards', text: 'Validator group rewards',
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
......
...@@ -9,11 +9,12 @@ interface Props { ...@@ -9,11 +9,12 @@ interface Props {
noTooltip?: boolean; noTooltip?: boolean;
tooltipInteractive?: boolean; tooltipInteractive?: boolean;
type?: 'long' | 'short'; type?: 'long' | 'short';
maxSymbols?: number;
as?: React.ElementType; as?: React.ElementType;
} }
const HashStringShorten = ({ hash, noTooltip, as = 'span', type, tooltipInteractive }: Props) => { const HashStringShorten = ({ hash, noTooltip, as = 'span', type, tooltipInteractive, maxSymbols }: Props) => {
const charNumber = type === 'long' ? 16 : 8; const charNumber = maxSymbols ?? (type === 'long' ? 16 : 8);
if (hash.length <= charNumber) { if (hash.length <= charNumber) {
return <chakra.span as={ as }>{ hash }</chakra.span>; return <chakra.span as={ as }>{ hash }</chakra.span>;
} }
......
...@@ -35,6 +35,7 @@ export interface EntityBaseProps { ...@@ -35,6 +35,7 @@ export interface EntityBaseProps {
tailLength?: number; tailLength?: number;
target?: React.HTMLAttributeAnchorTarget; target?: React.HTMLAttributeAnchorTarget;
truncation?: Truncation; truncation?: Truncation;
truncationMaxSymbols?: number;
variant?: 'content' | 'heading' | 'subheading'; variant?: 'content' | 'heading' | 'subheading';
linkVariant?: LinkProps['variant']; linkVariant?: LinkProps['variant'];
} }
...@@ -167,7 +168,9 @@ const IconShield = (props: IconShieldProps) => { ...@@ -167,7 +168,9 @@ const IconShield = (props: IconShieldProps) => {
return <IconSvg className="entity__shield" { ...styles } { ...svgProps }/>; 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; asProp?: React.ElementType;
text: string; text: string;
tooltipInteractive?: boolean; tooltipInteractive?: boolean;
...@@ -179,6 +182,7 @@ const Content = chakra(({ ...@@ -179,6 +182,7 @@ const Content = chakra(({
asProp, asProp,
text, text,
truncation = 'dynamic', truncation = 'dynamic',
truncationMaxSymbols,
tailLength, tailLength,
variant, variant,
noTooltip, noTooltip,
...@@ -208,6 +212,7 @@ const Content = chakra(({ ...@@ -208,6 +212,7 @@ const Content = chakra(({
type="long" type="long"
noTooltip={ noTooltip } noTooltip={ noTooltip }
tooltipInteractive={ tooltipInteractive } tooltipInteractive={ tooltipInteractive }
maxSymbols={ truncationMaxSymbols }
/> />
); );
case 'constant': case 'constant':
...@@ -217,6 +222,7 @@ const Content = chakra(({ ...@@ -217,6 +222,7 @@ const Content = chakra(({
as={ asProp } as={ asProp }
noTooltip={ noTooltip } noTooltip={ noTooltip }
tooltipInteractive={ tooltipInteractive } tooltipInteractive={ tooltipInteractive }
maxSymbols={ truncationMaxSymbols }
/> />
); );
case 'dynamic': case 'dynamic':
......
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 = { ...@@ -26,10 +26,10 @@ export type Props = {
icon?: IconName; 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) { if (href) {
return ( return (
<Link href={ route(href) } variant="plain"> <Link href={ route(href) } variant="plain" className={ className }>
{ children } { children }
</Link> </Link>
); );
...@@ -54,9 +54,9 @@ const StatsWidget = ({ ...@@ -54,9 +54,9 @@ const StatsWidget = ({
href, href,
}: Props) => { }: Props) => {
return ( return (
<Container href={ !isLoading ? href : undefined }> <Container href={ !isLoading ? href : undefined } className={ href ? className : undefined }>
<Flex <Flex
className={ className } className={ href ? undefined : className }
alignItems="center" alignItems="center"
bgColor={ isLoading ? { _light: 'blackAlpha.50', _dark: 'whiteAlpha.50' } : { _light: 'gray.50', _dark: 'whiteAlpha.100' } } bgColor={ isLoading ? { _light: 'blackAlpha.50', _dark: 'whiteAlpha.50' } : { _light: 'gray.50', _dark: 'whiteAlpha.100' } }
p={ 3 } p={ 3 }
......
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;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment