Skip to content

Commit

Permalink
Define an icon for each validator (#927)
Browse files Browse the repository at this point in the history
* Highlight the validator ID separately from the prefix

* Render custom icons for delegation and unbonding tokens

* Add todo

* Remove unnecessary class

* Disable selecting text

* Update CI 1/2 in the asset registry
  • Loading branch information
jessepinho authored Apr 12, 2024
1 parent 13d0bc5 commit 00183ba
Show file tree
Hide file tree
Showing 12 changed files with 310 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { memo, useMemo } from 'react';
import {
getDisplayDenomFromView,
getEquivalentValues,
getMetadata,
getValidatorInfoFromValueView,
} from '@penumbra-zone/getters/src/value-view';
import { asValueView } from '@penumbra-zone/getters/src/equivalent-value';
Expand Down Expand Up @@ -38,6 +39,7 @@ export const DelegationValueView = memo(
unstakedTokens?: ValueView;
}) => {
const validatorInfo = getValidatorInfoFromValueView(valueView);
const metadata = getMetadata(valueView);

const equivalentValueOfStakingToken = useMemo(() => {
const equivalentValue = getEquivalentValues(valueView).find(
Expand All @@ -54,6 +56,7 @@ export const DelegationValueView = memo(
<ValidatorInfoComponent
validatorInfo={validatorInfo}
votingPowerAsIntegerPercentage={votingPowerAsIntegerPercentage}
delegationTokenMetadata={metadata}
/>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ValidatorInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb';
import { Identicon } from '@penumbra-zone/ui/components/ui/identicon';
import { IdentityKeyComponent } from '@penumbra-zone/ui/components/ui/identity-key-component';
import {
Tooltip,
Expand All @@ -12,8 +11,9 @@ import {
getIdentityKeyFromValidatorInfo,
getValidator,
} from '@penumbra-zone/getters/src/validator-info';
import { bech32IdentityKey } from '@penumbra-zone/bech32/src/identity-key';
import { calculateCommissionAsPercentage } from '@penumbra-zone/types/src/staking';
import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { AssetIcon } from '@penumbra-zone/ui/components/ui/tx/view/asset-icon';

/**
* Renders a single `ValidatorInfo`: its name, bech32-encoded identity key,
Expand All @@ -22,9 +22,11 @@ import { calculateCommissionAsPercentage } from '@penumbra-zone/types/src/stakin
export const ValidatorInfoComponent = ({
validatorInfo,
votingPowerAsIntegerPercentage,
delegationTokenMetadata,
}: {
validatorInfo: ValidatorInfo;
votingPowerAsIntegerPercentage?: number;
delegationTokenMetadata: Metadata;
}) => {
// The tooltip component is a bit heavy to render, so we'll wait to render it
// until all loading completes.
Expand All @@ -36,12 +38,7 @@ export const ValidatorInfoComponent = ({
<TooltipProvider>
<div className='flex items-center gap-4'>
<div className='shrink-0'>
<Identicon
uniqueIdentifier={bech32IdentityKey(identityKey)}
size={48}
className='rounded-full'
type='gradient'
/>
<AssetIcon metadata={delegationTokenMetadata} size='lg' />
</div>

<div className='flex min-w-0 shrink flex-col gap-1'>
Expand Down
7 changes: 1 addition & 6 deletions apps/node-status/src/components/node-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,7 @@ export const NodeInfo = () => {
<div className='mb-2 flex flex-col gap-1'>
<strong>Network</strong>
<div className='flex items-center gap-2'>
<Identicon
uniqueIdentifier={nodeInfo.network}
type='gradient'
className='rounded-full'
size={14}
/>
<Identicon uniqueIdentifier={nodeInfo.network} type='gradient' size={14} />
<span className='text-2xl font-bold'>{nodeInfo.network}</span>
</div>
<strong>Version</strong>
Expand Down
48 changes: 30 additions & 18 deletions packages/constants/src/local-asset-registry.json

Large diffs are not rendered by default.

7 changes: 1 addition & 6 deletions packages/ui/components/ui/address-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,5 @@ interface AddressIconProps {
* A simple component to display a consistently styled icon for a given address.
*/
export const AddressIcon = ({ address, size }: AddressIconProps) => (
<Identicon
uniqueIdentifier={bech32Address(address)}
size={size}
className='rounded-full'
type='gradient'
/>
<Identicon uniqueIdentifier={bech32Address(address)} size={size} type='gradient' />
);
8 changes: 4 additions & 4 deletions packages/ui/components/ui/identicon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const Identicon = ({ type, ...props }: IdenticonProps & { type: 'gradient
return <IdenticonSolid {...props} />;
};

const IdenticonGradient = ({ uniqueIdentifier, size = 120, className }: IdenticonProps) => {
const IdenticonGradient = ({ uniqueIdentifier, size = 120 }: IdenticonProps) => {
const gradient = useMemo(() => generateGradient(uniqueIdentifier), [uniqueIdentifier]);
const gradientId = useMemo(() => `gradient-${uniqueIdentifier}`, [uniqueIdentifier]);

Expand All @@ -18,7 +18,7 @@ const IdenticonGradient = ({ uniqueIdentifier, size = 120, className }: Identico
viewBox={`0 0 ${size} ${size}`}
version='1.1'
xmlns='http://www.w3.org/2000/svg'
className={className}
className='rounded-full'
>
<g>
<defs>
Expand All @@ -33,7 +33,7 @@ const IdenticonGradient = ({ uniqueIdentifier, size = 120, className }: Identico
);
};

const IdenticonSolid = ({ uniqueIdentifier, size = 120, className }: IdenticonProps) => {
const IdenticonSolid = ({ uniqueIdentifier, size = 120 }: IdenticonProps) => {
const color = useMemo(() => generateSolidColor(uniqueIdentifier), [uniqueIdentifier]);

return (
Expand All @@ -43,7 +43,7 @@ const IdenticonSolid = ({ uniqueIdentifier, size = 120, className }: IdenticonPr
viewBox={`0 0 ${size} ${size}`}
version='1.1'
xmlns='http://www.w3.org/2000/svg'
className={className}
className='rounded-full'
>
<rect fill={color.bg} x='0' y='0' width={size} height={size} />
<text
Expand Down
9 changes: 7 additions & 2 deletions packages/ui/components/ui/identity-key-component.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { IdentityKey } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb';
import { CopyToClipboardIconButton } from './copy-to-clipboard-icon-button';
import { bech32IdentityKey } from '@penumbra-zone/bech32/src/identity-key';
import { PENUMBRA_BECH32_IDENTITY_PREFIX } from '@penumbra-zone/bech32/src/penumbra-bech32';

const PREFIX_AND_SEPARATOR = PENUMBRA_BECH32_IDENTITY_PREFIX + '1';

/**
* Renders a validator's `IdentityKey` as a bech32-encoded string, along with a
Expand All @@ -14,8 +17,10 @@ import { bech32IdentityKey } from '@penumbra-zone/bech32/src/identity-key';
export const IdentityKeyComponent = ({ identityKey }: { identityKey: IdentityKey }) => {
return (
<div className='flex min-w-0 items-center gap-2'>
<div className='min-w-0 truncate font-mono text-muted-foreground'>
{bech32IdentityKey(identityKey)}
<div className='min-w-0 truncate font-mono'>
<span className='text-muted-foreground'>{PREFIX_AND_SEPARATOR}</span>

{bech32IdentityKey(identityKey).replace(PREFIX_AND_SEPARATOR, '')}
</div>

<CopyToClipboardIconButton text={bech32IdentityKey(identityKey)} />
Expand Down
22 changes: 0 additions & 22 deletions packages/ui/components/ui/tx/view/asset-icon.tsx

This file was deleted.

106 changes: 106 additions & 0 deletions packages/ui/components/ui/tx/view/asset-icon/delegation-token-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { assetPatterns } from '@penumbra-zone/constants/src/assets';

const getFirstEightCharactersOfValidatorId = (displayDenom = ''): [string, string] => {
const id = (assetPatterns.delegationToken.capture(displayDenom)?.id ?? '').substring(0, 8);

const firstFour = id.substring(0, 4);
const lastFour = id.substring(4);

return [firstFour, lastFour];
};

export const DelegationTokenIcon = ({
className,
displayDenom,
}: {
className?: string;
displayDenom?: string;
}) => {
const [firstFour, lastFour] = getFirstEightCharactersOfValidatorId(displayDenom);

return (
<svg
id='delegation'
xmlns='http://www.w3.org/2000/svg'
xmlnsXlink='http://www.w3.org/1999/xlink'
viewBox='0 0 32 32'
className={className}
>
<defs>
<radialGradient
id='logoGradient'
cx='-475.62'
cy='477.46'
fx='-475.62'
fy='477.46'
r='1'
gradientTransform='translate(8030.1 3047.46) rotate(-24.39) scale(12.71 -12.71)'
gradientUnits='userSpaceOnUse'
>
<stop offset='0' stopColor='#f79036' />
<stop offset='.6' stopColor='#f79036' />
<stop offset='.88' stopColor='#96d5d1' />
<stop offset='1' stopColor='#96d5d1' />
</radialGradient>
</defs>
<rect id='background' width='32' height='32' fill='#000' strokeWidth='0' />
<path
id='logo'
d='M21.04,4.16c-.87.31-1.73.71-2.55,1.09h0c-1.51.7-2.94,1.36-4.07,1.29h0c-.4-.02-.88-.08-1.44-.16h0c-2.12-.26-5.02-.63-6.54.61h0c-1.08.88-.91,2.97-.75,5h0c.12,1.52.25,3.08-.17,4.02h0c-.94,2.13-1.26,3.94-.95,5.39h0c.3,1.42,1.22,2.54,2.71,3.31h0c.84.44,1.62.62,2.44.81h0c1.2.28,2.44.58,3.99,1.71h0c.77.57,1.57.85,2.42.85h0c1.61,0,3.4-1.04,5.53-3.16h0c.77-.77,1.72-1.22,2.63-1.66h0c1.07-.51,2.18-1.04,3.03-2.09h0c.52-.64.65-1.33.4-2.09h0c-.22-.68-.7-1.36-1.21-2.08h0c-.56-.79-1.13-1.6-1.48-2.53h0c-.6-1.62-.49-2.26-.29-3.44h0c.13-.76.3-1.71.33-3.25h0c.04-1.76-.45-2.76-1.26-3.34h0c-.43-.31-.93-.51-1.55-.51h0c-.37,0-.78.07-1.24.24M11.78,21.76c-1.18-.87-2.01-2.07-2.39-3.47h0c-.46-1.67-.23-3.41.65-4.9h0c.88-1.5,2.3-2.57,4.01-3.02h0c.56-.15,1.14-.23,1.72-.23h0c1.42,0,2.84.47,3.98,1.31h0c1.18.87,2.01,2.07,2.39,3.47h0c.46,1.67.23,3.41-.65,4.9h0c-.88,1.5-2.3,2.57-4.01,3.02h0c-.57.15-1.15.23-1.72.23h0c-1.42,0-2.84-.47-3.98-1.31'
fill='url(#logoGradient)'
opacity='.4'
strokeWidth='0'
/>
<text
id='id'
transform='translate(4.8 13.83)'
fill='#96d5d1'
fontFamily="Iosevka-Term, 'Iosevka Term'"
fontSize='11.06'
pointerEvents='none'
>
<tspan x='0' y='0'>
{firstFour}
</tspan>
<tspan x='0' y='10'>
{lastFour}
</tspan>
</text>
<g id='arrow'>
<line
x1='20.79'
y1='27.45'
x2='25.92'
y2='27.45'
fill='none'
stroke='#96d5d1'
strokeLinecap='round'
strokeMiterlimit='10'
strokeWidth='.75'
/>
<line
x1='24.1'
y1='25.92'
x2='25.92'
y2='27.45'
fill='none'
stroke='#96d5d1'
strokeLinecap='round'
strokeMiterlimit='10'
strokeWidth='.75'
/>
<line
x1='24.1'
y1='28.98'
x2='25.92'
y2='27.45'
fill='none'
stroke='#96d5d1'
strokeLinecap='round'
strokeMiterlimit='10'
strokeWidth='.75'
/>
</g>
</svg>
);
};
46 changes: 46 additions & 0 deletions packages/ui/components/ui/tx/view/asset-icon/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { Identicon } from '../../../identicon';
import { cn } from '../../../../../lib/utils';
import { DelegationTokenIcon } from './delegation-token-icon';
import { getDisplay } from '@penumbra-zone/getters/src/metadata';
import { assetPatterns } from '@penumbra-zone/constants/src/assets';
import { UnbondingTokenIcon } from './unbonding-token-icon';

export const AssetIcon = ({
metadata,
size = 'sm',
}: {
metadata?: Metadata;
size?: 'sm' | 'lg';
}) => {
// Image default is "" and thus cannot do nullish-coalescing
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const icon = metadata?.images[0]?.png || metadata?.images[0]?.svg;
const className = cn('rounded-full', size === 'sm' && 'size-6', size === 'lg' && 'size-12');
const display = getDisplay.optional()(metadata);
const isDelegationToken = display ? assetPatterns.delegationToken.matches(display) : false;
const isUnbondingToken = display ? assetPatterns.unbondingToken.matches(display) : false;

return (
<>
{icon ? (
<img className={className} src={icon} alt='Asset icon' />
) : isDelegationToken ? (
<DelegationTokenIcon displayDenom={display} className={className} />
) : isUnbondingToken ? (
/**
* @todo: Render a custom unbonding token for validators that have a
* logo -- e.g., with the validator ID superimposed over the validator
* logo.
*/
<UnbondingTokenIcon displayDenom={display} className={className} />
) : (
<Identicon
uniqueIdentifier={metadata?.symbol ?? '?'}
size={size === 'lg' ? 48 : 24}
type='solid'
/>
)}
</>
);
};
Loading

0 comments on commit 00183ba

Please sign in to comment.