Files

247 lines
8.0 KiB
TypeScript

import React from 'react';
import type { AddressParam } from 'types/api/addressParams';
import { route } from 'nextjs/routes';
import { toBech32Address } from 'lib/address/bech32';
import { useAddressHighlightContext } from 'lib/contexts/addressHighlight';
import { useSettingsContext } from 'lib/contexts/settings';
import { Skeleton } from '@luxfi/ui/skeleton';
import { Tooltip } from '@luxfi/ui/tooltip';
import * as EntityBase from 'ui/shared/entities/base/components';
import { getTagName } from 'ui/shared/EntityTags/utils';
import getChainTooltipText from 'ui/shared/externalChains/getChainTooltipText';
import type { IconName } from 'ui/shared/IconSvg';
import { distributeEntityProps, getContentProps, getIconProps } from '../base/utils';
import AddressEntityContentProxy from './AddressEntityContentProxy';
import AddressIconDelegated from './AddressIconDelegated';
import AddressIdenticon from './AddressIdenticon';
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'address'>;
const getDisplayedAddress = (address: AddressProp, altHash?: string) => {
return address.filecoin?.robust ?? address.filecoin?.id ?? altHash ?? address.hash;
};
const Link = (props: LinkProps) => {
const defaultHref = route(
{ pathname: '/address/[hash]', query: { ...props.query, hash: props.address.hash } },
{ chain: props.chain, external: props.external },
);
return (
<EntityBase.Link
{ ...props }
href={ props.href ?? defaultHref }
>
{ props.children }
</EntityBase.Link>
);
};
type IconProps = Pick<EntityProps, 'address' | 'isSafeAddress'> & EntityBase.IconBaseProps;
const Icon = (props: IconProps) => {
if (props.noIcon) {
return null;
}
const shield = (() => {
if ('shield' in props) {
return props.shield;
}
if (props.chain) {
return props.chain.logo ? { src: props.chain.logo } : { name: 'networks/icon-placeholder' as const };
}
})();
const hintPostfix: string = props.hintPostfix ?? (props.chain && props.shield !== false ? getChainTooltipText(props.chain, ' on ') : '');
const styles = getIconProps(props, Boolean(shield));
if (props.isLoading) {
return <Skeleton boxSize={ styles.boxSize } marginRight={ String(styles.marginRight) } loading borderRadius="full" flexShrink={ 0 }/>;
}
if ('src' in props || 'name' in props) {
return <EntityBase.Icon { ...props } shield={ shield }/>;
}
const isDelegatedAddress = props.address.proxy_type === 'eip7702';
if (props.address.is_contract && !isDelegatedAddress) {
if (props.isSafeAddress) {
return (
<EntityBase.Icon
{ ...props }
shield={ shield }
name="brands/safe"
/>
);
}
const isProxy = Boolean(props.address.implementations?.length);
const isVerified = isProxy ? props.address.is_verified && props.address.implementations?.every(({ name }) => Boolean(name)) : props.address.is_verified;
const contractIconName: IconName = props.address.is_verified ? 'contracts/verified' : 'contracts/regular';
const label = (isVerified ? 'verified ' : '') + (isProxy ? 'proxy contract' : 'contract') + hintPostfix;
return (
<EntityBase.Icon
{ ...props }
shield={ shield }
name={ isProxy ? 'contracts/proxy' : contractIconName }
color={ isVerified ? 'green.500' : undefined }
borderRadius={ 0 }
hint={ label.slice(0, 1).toUpperCase() + label.slice(1) }
/>
);
}
const label = (() => {
if (isDelegatedAddress) {
return (props.address.is_verified ? 'EOA + verified code' : 'EOA + code') + hintPostfix;
}
if (props.chain) {
return 'Address' + hintPostfix;
}
return props.hint;
})();
return (
<Tooltip
content={ label }
disabled={ !label }
interactive={ props.tooltipInteractive }
positioning={ shield ? { offset: { mainAxis: 8 } } : undefined }
>
<div style={{ marginRight: styles.marginRight, position: 'relative' }}>
<AddressIdenticon
size={ props.size ?? (props.variant === 'heading' ? 30 : 20) }
hash={ getDisplayedAddress(props.address) }
/>
{ shield && <EntityBase.IconShield { ...shield }/> }
{ isDelegatedAddress && <AddressIconDelegated isVerified={ Boolean(props.address.is_verified) }/> }
</div>
</Tooltip>
);
};
export type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'address'> & { altHash?: string };
const Content = (props: ContentProps) => {
const displayedAddress = getDisplayedAddress(props.address, props.altHash);
const nameTag = (() => {
const tagData = props.address.metadata?.tags.find(tag => tag.tagType === 'name');
if (!tagData || !tagData.name) {
return;
}
return getTagName(tagData, props.address.hash);
})();
const nameText = nameTag || props.address.ens_domain_name || props.address.name;
const isProxy = props.address.implementations && props.address.implementations.length > 0 && props.address.proxy_type !== 'eip7702';
if (isProxy) {
return <AddressEntityContentProxy { ...props }/>;
}
if (nameText) {
const styles = getContentProps(props.variant);
const label = (
<div className="flex flex-col gap-0 py-1 text-inherit">
<div className="font-semibold whitespace-pre-wrap break-words">{ nameText }</div>
<div className="whitespace-pre-wrap break-words">
{ displayedAddress }
</div>
</div>
);
return (
<Tooltip
content={ label }
contentProps={{ className: 'max-w-[calc(100vw-8px)] lg:max-w-[400px]' }}
triggerProps={{ className: 'min-w-0' }}
interactive={ props.tooltipInteractive }
disabled={ props.noTooltip }
>
<Skeleton loading={ props.isLoading } overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" className={ styles?.className }>
{ nameText }
</Skeleton>
</Tooltip>
);
}
return (
<EntityBase.Content
{ ...props }
text={ displayedAddress }
/>
);
};
type CopyProps = Omit<EntityBase.CopyBaseProps, 'text'> & Pick<EntityProps, 'address'> & { altHash?: string };
const Copy = (props: CopyProps) => {
return (
<EntityBase.Copy
{ ...props }
text={ getDisplayedAddress(props.address, props.altHash) }
/>
);
};
const Container = EntityBase.Container;
interface AddressProp extends Partial<AddressParam> {
hash: string;
}
export interface EntityProps extends EntityBase.EntityBaseProps {
address: AddressProp;
isSafeAddress?: boolean;
noHighlight?: boolean;
noAltHash?: boolean;
}
const AddressEntity = (props: EntityProps) => {
const partsProps = distributeEntityProps(props);
const highlightContext = useAddressHighlightContext(props.noHighlight);
const settingsContext = useSettingsContext();
const altHash = !props.noAltHash && settingsContext?.addressFormat === 'bech32' ? toBech32Address(props.address.hash) : undefined;
// inside highlight context all tooltips should be interactive
// because non-interactive ones will not pass 'onMouseLeave' event to the parent component
const content = <Content { ...partsProps.content } altHash={ altHash } tooltipInteractive={ Boolean(highlightContext) }/>;
return (
<Container
// we have to use the global classnames here, see theme/global.ts
className={ `${ props.className } address-entity ${ props.noCopy ? 'address-entity_no-copy' : '' } relative z-0 w-fit max-w-full` }
data-hash={ highlightContext && !props.isLoading ? props.address.hash : undefined }
onMouseEnter={ highlightContext?.onMouseEnter }
onMouseLeave={ highlightContext?.onMouseLeave }
>
<Icon { ...partsProps.icon } tooltipInteractive={ Boolean(highlightContext) }/>
{ props.noLink ? content : <Link { ...partsProps.link }>{ content }</Link> }
<Copy { ...partsProps.copy } altHash={ altHash } tooltipInteractive={ Boolean(highlightContext) }/>
</Container>
);
};
export default React.memo(AddressEntity);
export {
Container,
Link,
Icon,
Content,
Copy,
};