Celo Account info on Address details page (#2966)

* Celo Account info on Address details page

Fixes #2962

* add signers information and change hints placement
This commit is contained in:
tom goriunov
2025-09-17 16:15:20 +02:00
committed by GitHub
parent e5a10cd029
commit 8e6fee29f7
7 changed files with 178 additions and 0 deletions
+14
View File
@@ -20,6 +20,7 @@ export interface Address extends UserTags {
ens_domain_name: string | null;
filecoin?: AddressFilecoinParams;
zilliqa?: AddressZilliqaParams;
celo?: AddressCeloParams;
// TODO: if we are happy with tabs-counters method, should we delete has_something fields?
has_beacon_chain_withdrawals?: boolean;
has_logs: boolean;
@@ -40,6 +41,19 @@ export interface AddressZilliqaParams {
is_scilla_contract: boolean;
}
export interface AddressCeloParams {
account: {
locked_celo: string;
metadata_url: string | null;
name: string | null;
nonvoting_locked_celo: string;
type: string;
vote_signer_address: AddressParam | null;
validator_signer_address: AddressParam | null;
attestation_signer_address: AddressParam | null;
} | null;
}
export interface AddressCounters {
transactions_count: string;
token_transfers_count: string;
+7
View File
@@ -23,6 +23,7 @@ import Address3rdPartyWidgets from './Address3rdPartyWidgets';
import useAddress3rdPartyWidgets from './address3rdPartyWidgets/useAddress3rdPartyWidgets';
import AddressAlternativeFormat from './details/AddressAlternativeFormat';
import AddressBalance from './details/AddressBalance';
import AddressCeloAccount from './details/AddressCeloAccount';
import AddressImplementations from './details/AddressImplementations';
import AddressNameInfo from './details/AddressNameInfo';
import AddressNetWorth from './details/AddressNetWorth';
@@ -62,6 +63,7 @@ const AddressDetails = ({ addressQuery, countersQuery, isLoading }: Props) => {
has_token_transfers: true,
has_validated_blocks: false,
filecoin: undefined,
celo: undefined,
creator_filecoin_robust_address: null,
creator_address_hash: null,
}), [ addressHash ]);
@@ -90,6 +92,11 @@ const AddressDetails = ({ addressQuery, countersQuery, isLoading }: Props) => {
<>
{ addressQuery.isDegradedData && <ServiceDegradationWarning isLoading={ isLoading } mb={ 6 }/> }
<DetailedInfo.Container>
{ data.celo?.account && (
<AddressCeloAccount data={ data.celo.account } isLoading={ isLoading }/>
) }
<AddressAlternativeFormat isLoading={ isLoading } addressHash={ addressHash }/>
{ data.filecoin?.id && (
@@ -0,0 +1,29 @@
import React from 'react';
import * as addressMock from 'mocks/address/address';
import { test, expect } from 'playwright/lib';
import { Container } from 'ui/shared/DetailedInfo/DetailedInfo';
import AddressCeloAccount from './AddressCeloAccount';
const DATA = {
name: 'CLabs Validator #2 on alfajores',
type: 'validator',
locked_celo: '10000000000000000000000',
// eslint-disable-next-line max-len
metadata_url: 'https://storage.googleapis.com/clabs_validator_metadata/alfajores/validator-alfajores-0x050f34537F5b2a00B9B9C752Cb8500a3fcE3DA7d-metadata.json',
nonvoting_locked_celo: '10000000000000000000000',
vote_signer_address: addressMock.withName,
validator_signer_address: addressMock.withEns,
attestation_signer_address: addressMock.withoutName,
};
test('default view +@mobile', async({ render }) => {
const component = await render(
<Container>
<AddressCeloAccount data={ DATA }/>
</Container>,
);
await component.getByText('View details').click();
await expect(component).toHaveScreenshot();
});
+118
View File
@@ -0,0 +1,118 @@
import { Grid, GridItem } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { upperFirst } from 'es-toolkit';
import React from 'react';
import type { Address } from 'types/api/address';
import type { ExcludeNull, ExcludeUndefined } from 'types/utils';
import config from 'configs/app';
import { currencyUnits } from 'lib/units';
import { CollapsibleDetails } from 'toolkit/chakra/collapsible';
import { Link } from 'toolkit/chakra/link';
import { Hint } from 'toolkit/components/Hint/Hint';
import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TruncatedValue from 'ui/shared/TruncatedValue';
const AddressCeloAccountItem = ({ label, hint, children }: { label: string; hint: string; children: React.ReactNode }) => {
return (
<>
<GridItem color="text.secondary" display="flex" alignItems="center">
<Hint label={ hint } boxSize={ 4 } mr={ 1 }/>
<TruncatedValue value={ label } maxW={{ base: '130px', lg: 'unset' }}/>
</GridItem>
{ children }
</>
);
};
interface Props {
isLoading?: boolean;
data: ExcludeNull<ExcludeUndefined<Address['celo']>['account']>;
}
const AddressCeloAccount = ({ isLoading, data }: Props) => {
return (
<>
<DetailedInfo.ItemLabel
hint="Celo account info (from the Accounts contract), including it's human-readable name"
isLoading={ isLoading }
>
Celo account
</DetailedInfo.ItemLabel>
<DetailedInfo.ItemValue multiRow>
{ data.name && <TruncatedValue value={ data.name } mr={ 3 }/> }
<CollapsibleDetails noScroll variant="secondary" display="block" textStyle={ undefined } fontSize="sm" loading={ isLoading }>
<Grid
gridTemplateColumns="max-content minmax(0px, 1fr)"
textStyle="sm"
bgColor={{ _light: 'blackAlpha.50', _dark: 'whiteAlpha.50' }}
w="100%"
p={{ base: 2, lg: 3 }}
mt={ 1 }
columnGap={ 3 }
rowGap={ 4 }
borderBottomRightRadius="base"
borderBottomLeftRadius="base"
>
<AddressCeloAccountItem label="Type" hint="The role of the account: regular, validator, or validator group">
{ upperFirst(data.type) }
</AddressCeloAccountItem>
{ data.metadata_url && (
<AddressCeloAccountItem label="Metadata URL" hint="Link to additional information published by the account owner">
<Link href={ data.metadata_url } external>
<TruncatedValue value={ data.metadata_url }/>
</Link>
</AddressCeloAccountItem>
) }
<AddressCeloAccountItem
label={ `Locked ${ currencyUnits.ether }` }
hint="Total amount of CELO locked by this account (used for staking or governance)"
>
<TruncatedValue value={ BigNumber(data.locked_celo).div(10 ** config.chain.currency.decimals).toFormat() }/>
</AddressCeloAccountItem>
<AddressCeloAccountItem
label={ `Non-voting locked ${ currencyUnits.ether }` }
hint="Portion of locked CELO that is not currently used for voting"
>
<TruncatedValue value={ BigNumber(data.nonvoting_locked_celo).div(10 ** config.chain.currency.decimals).toFormat() }/>
</AddressCeloAccountItem>
{ data.vote_signer_address && (
<AddressCeloAccountItem
label="Vote signer address"
hint="Address authorized to vote in governance and validator elections on behalf of this account"
>
<AddressEntity address={ data.vote_signer_address }/>
</AddressCeloAccountItem>
) }
{ data.validator_signer_address && (
<AddressCeloAccountItem
label="Validator signer address"
hint="Address authorized to manage a validator or validator group and sign consensus messages for this account"
>
<AddressEntity address={ data.validator_signer_address }/>
</AddressCeloAccountItem>
) }
{ data.attestation_signer_address && (
<AddressCeloAccountItem
label="Attestation signer address"
hint="Address whose key this account uses to sign attestations on the Attestations contract"
>
<AddressEntity address={ data.attestation_signer_address }/>
</AddressCeloAccountItem>
) }
</Grid>
</CollapsibleDetails>
</DetailedInfo.ItemValue>
</>
);
};
export default React.memo(AddressCeloAccount);
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

+10
View File
@@ -323,6 +323,16 @@ const AddressPageContent = () => {
const tags: Array<EntityTag> = React.useMemo(() => {
return [
...(addressQuery.data?.public_tags?.map((tag) => ({ slug: tag.label, name: tag.display_name, tagType: 'custom' as const, ordinal: -1 })) || []),
addressQuery.data?.celo?.account ? {
slug: 'celo-account',
name: 'Celo account',
tagType: 'custom' as const,
ordinal: PREDEFINED_TAG_PRIORITY,
meta: {
bgColor: 'yellow.200',
textColor: 'black',
},
} : undefined,
!addressQuery.data?.is_contract ? { slug: 'eoa', name: 'EOA', tagType: 'custom' as const, ordinal: PREDEFINED_TAG_PRIORITY } : undefined,
config.features.validators.isEnabled && addressQuery.data?.has_validated_blocks ?
{ slug: 'validator', name: 'Validator', tagType: 'custom' as const, ordinal: PREDEFINED_TAG_PRIORITY } :