Files
edc54514f1 OP Superchain explorer: aggregated views (#3040)
* refactor widgets on home page

* coming soon placeholder component

* top accounts page

* contract data

* address ERC-20 tokens list

* chain filter for address ERC-20 tokens

* token select for address detailed view

* address details fields

* tokens table

* stats widgets on home page

* chain indicators graph on the home page

* quick search

* search results all tab

* tabs and chain select on search results page

* search results mobile view

* add contract info to search and address details

* add text filter for tokens and search improvements

* load all results inside tab on search results page

* token balance info

* add support for domains in the full search

* Multichain explorer

Resolves #2989

* add envs validator for multichain

* add some tests

* fix search bar on search results page on mobile

* home stats fixes

* fix ts

* optimize amount of queries on address page

* add more contract info on address page

* add enter key hint to search input

* fix tests

* fix test

* update bg.primary css-variable on color theme change

* sitemap for multichain

* txs page: render first tab if blob tab is not available on the chain

* filter out in active chains from chain select on address page and user ops page

* add chain icon to advanced filter table

* fix tooltip on entity with shield

* fix ts

* show contract implementations

* fixes

* add chain icon to all lists

* refactor zetachain chains config type

* use chain id as primary option instead of chain slug

* refactor chain config type for multichain and essential dapps

* refactor chain icon and chain select

* rename chain-slug in chain_slug

* change default radius of chain icon and update screenshots for multichain tests

* change conditions for "isContract" and "isVerified"

* test fixes

* make shield optional for external chain entities

* add chain icon to approvals lists

* add stats page for multichain

* add chain postfix to chart .png and .csv assets name

* fix tests

* sync verified contracts language filter and fix loading state in the search results page

* fix sol2uml diagram and csv export

* regcred from helmfile for review

* link fixes

* fix state for unknown address

* fix incorrect chain in the context during spa transition to the same route on different chain

* add links to stats widgets

* add mixpanel to L2 demo values

* show message of on-demand contract verification

* fix screenshots

---------

Co-authored-by: Nick Zenchik <[email protected]>
2025-11-18 09:44:22 +01:00

549 lines
16 KiB
TypeScript

import { Box } from '@chakra-ui/react';
import { MultisenderWidget } from '@multisender.app/multisender-react-widget';
import React from 'react';
import { getFeaturePayload } from 'configs/app/features/types';
import config from 'configs/app';
import essentialDappsChainsConfig from 'configs/essential-dapps-chains';
import useIsMobile from 'lib/hooks/useIsMobile';
import AdBanner from 'ui/shared/ad/AdBanner';
const feature = getFeaturePayload(config.features.marketplace);
const dappConfig = feature?.essentialDapps?.multisend;
const Container = ({ children }: { children: React.ReactNode }) => (
<Box
w="full"
maxW="670px"
mx="auto"
css={{
'& > .multisenderTheme': {
'--mw-color-brand': { _light: 'colors.blue.600', _dark: 'colors.blue.500' },
'--mw-color-brand-stroke': { _light: 'colors.gray.200', _dark: 'colors.whiteAlpha.100' },
'--mw-color-brand-text-secondary': 'colors.text.secondary',
'--mw-color-brand-fill': 'colors.dialog.bg', // modal background
fontFamily: 'body',
'& [class*="_stepBody_"] > span': {
fontSize: 'xs',
fontWeight: 'normal',
},
'& [class*="_step_"]:not([data-progress="true"]), & [class*="_step_"][data-completed="true"]': {
'& [class*="_stepBody_"] > span': {
color: 'text.secondary',
},
'& [class*="_stepIcon_"]': {
backgroundColor: { _light: 'gray.100', _dark: 'whiteAlpha.100' },
},
'& [class*="_stepCompletedIcon_"]': {
color: 'text.secondary',
},
},
'& [class*="_stepIcon_"]': {
fontSize: 'xs',
width: '24px',
height: '24px',
minWidth: '24px',
minHeight: '24px',
},
'& [class*="_stepSeparator_"]': {
height: '1px',
},
'& [class*="_stepperRoot_"] > :nth-child(1)': {
padding: '0 12px',
},
'& [class*="_fieldsetRoot_"]:not([class*="_itemRoot_"])': {
borderRadius: 'md',
padding: '24px',
'& button': {
fontSize: 'xs',
'& label': {
fontWeight: 'medium !important',
},
},
'& > *:first-child': {
gap: '12px',
'& > :nth-child(4) > button': {
marginBottom: '12px',
border: '2px solid',
borderColor: 'button.outline.fg',
borderRadius: 'base',
color: 'button.outline.fg',
height: '32px',
padding: '0 12px',
fontSize: 'sm',
transitionProperty: 'all',
transitionDuration: 'moderate',
'&:hover': {
background: 'none',
color: 'hover',
borderColor: 'hover',
},
'&:active': {
transform: 'none',
},
},
},
'& > *:last-child': {
'& p[data-size="sm"], & a[data-size="sm"]': {
fontSize: 'xs',
},
'& a[data-size="sm"]': {
color: 'link.primary',
fontWeight: 'medium !important',
'&:hover': {
color: 'link.primary.hover',
},
},
},
'& p[data-size="xl"], & h5': {
fontFamily: 'heading',
fontSize: '18px',
fontWeight: 'medium !important',
},
'& [class*="_tableTr_"]': {
fontWeight: 'medium',
'& td': {
padding: '12px 0',
},
'& a': {
color: 'link.primary',
fontSize: 'sm',
'&:hover': {
color: 'link.primary.hover',
textDecoration: 'none',
},
},
},
'& [class*="SummaryList-module__root__"]': {
'& + div': {
gap: '4px',
'& > div:last-child': {
'& button': {
background: 'none',
border: 'none',
color: 'button.outline.fg',
transitionProperty: 'all',
transitionDuration: 'moderate',
'&:hover': {
color: 'hover',
},
'&:active': {
transform: 'none',
},
},
'& p': {
fontSize: 'sm',
color: 'text.secondary',
},
},
},
'& + button': {
marginTop: '12px',
},
'& span': {
'--loader-color': 'var(--chakra-colors-text-primary) !important',
},
},
},
'& [class*="_itemRoot_"]': {
border: 'none',
borderRadius: 'base',
backgroundColor: { _light: 'gray.50', _dark: 'whiteAlpha.100' },
'& > div': {
gap: '8px',
},
},
'& [class*="_inputWrapper_"]': {
'& [class*="_input_"]': {
borderRadius: 'base',
border: '2px solid',
borderColor: 'input.border.filled',
height: '32px',
minHeight: '32px',
fontSize: 'sm',
fontWeight: 'medium',
color: 'input.fg',
'&::placeholder': {
color: 'input.placeholder',
},
},
'&[data-size="xl"]': {
'& [class*="_input_"]': {
height: '60px',
minHeight: '60px',
fontSize: 'md',
},
'& button': {
height: '40px',
fontSize: 'md',
backgroundColor: 'button.solid.bg',
borderRadius: 'base',
transitionProperty: 'all',
transitionDuration: 'moderate',
color: 'white',
'&:hover': {
backgroundColor: 'hover',
},
'&:active': {
transform: 'none',
},
'&:disabled': {
opacity: '0.2',
},
},
},
},
'& [class*="RecipientsEditor-module__head_"]': {
fontSize: 'xs',
marginTop: '12px',
'& p': {
fontWeight: 'medium',
},
'& button': {
background: 'none',
color: 'link.primary',
fontWeight: 'medium',
padding: 0,
height: '18px',
transitionProperty: 'all',
transitionDuration: 'moderate',
borderRadius: 0,
'&:hover': {
color: 'link.primary.hover',
},
'&:active': {
transform: 'none',
},
},
},
'& [class*="CodeMirror"]': {
fontSize: '15px',
'& .cm-scroller': {
borderRadius: 'base',
borderWidth: '2px',
borderColor: 'input.border.filled',
},
'& .cm-gutters': {
borderTopLeftRadius: '0',
borderBottomLeftRadius: '0',
borderRightWidth: '2px',
borderColor: 'input.border.filled',
backgroundColor: { _light: 'gray.50', _dark: 'whiteAlpha.50' },
backdropFilter: 'none',
'&:before': {
background: 'none',
},
},
'& .cm-placeholder': {
color: 'input.placeholder',
},
},
'& [class*="_modalRoot_"]': {
'& [class*="_modalOverlay_"]': {
backgroundColor: 'blackAlpha.800',
},
'& [class*="_modalContent_"]': {
borderRadius: 'xl',
},
'& [class*="_modalHeader_"]': {
minHeight: '64px',
height: '64px !important',
padding: '24px !important',
paddingBottom: '0 !important',
marginBottom: '8px',
'& [class*="_modalTitle_"]': {
fontFamily: 'heading',
fontWeight: 'medium !important',
fontSize: '24px !important',
},
},
'& [class*="_modalHeader_"] + div': {
padding: '24px !important',
paddingTop: '0 !important',
'& > div': {
padding: '0 !important',
},
'& h6, & p': {
fontSize: 'md',
},
},
'& [class*="_closeButton_"]': {
background: 'none',
color: 'close-button-fg',
'&:hover': {
color: 'hover',
},
},
},
'& div[role="alert"]': {
borderRadius: 'base',
padding: '8px 12px',
margin: '0 !important',
'& p, & div': {
fontSize: 'md',
color: 'text.primary !important',
},
'& [class*="_alertIcon_"]': {
marginTop: '2px',
marginRight: '8px',
'& svg': {
fill: 'text.primary',
width: '20px',
height: '20px',
},
},
'&:not([data-variant="filled"]) [class*="_alertIcon_"]': {
display: 'none',
},
'&[data-variant="filled"]': {
backgroundColor: 'alert.bg.info !important',
},
},
'& div[role="tooltip"]': {
backgroundColor: 'tooltip.bg',
color: 'tooltip.fg',
borderRadius: 'sm',
padding: '4px 8px !important',
'& p': {
fontSize: 'sm',
fontWeight: 'medium',
},
},
'& [class*="_button_"][data-size="lg"], [class*="_button_"][data-size="md"]': {
borderRadius: 'base',
color: 'white',
height: '40px',
backgroundColor: 'button.solid.bg',
transitionProperty: 'background-color',
transitionDuration: 'moderate',
'&:disabled': {
opacity: '0.2',
},
'&:hover': {
backgroundColor: 'hover',
},
'&:active': {
transform: 'none',
},
'& > span[aria-hidden="true"]': {
width: '20px',
height: '20px',
'& > span::after': {
width: '20px',
height: '20px',
},
},
},
'& [class*="Summary-module__upgradeBtn__"]': {
borderRadius: 'base',
border: 'none',
height: '32px',
padding: '0 12px',
fontSize: 'sm !important',
fontWeight: 'semibold',
transitionProperty: 'all',
transitionDuration: 'moderate',
'&[class*="_crown_"]': {
color: 'white',
backgroundColor: 'button.solid.bg',
'&:hover': {
backgroundColor: 'hover',
},
},
'&[class*="_gift_"]': {
color: 'button.outline.fg',
border: '2px solid',
borderColor: 'button.outline.fg',
marginRight: '-4px',
background: 'none',
'&:hover': {
color: 'hover',
borderColor: 'hover',
},
},
'& span[data-position="left"]': {
display: 'none',
},
},
'& [class*="Summary-module__editGasCost__"]': {
border: 'none',
background: 'none',
'& svg': {
width: '16px',
height: '16px',
color: 'link.primary',
transitionProperty: 'all',
transitionDuration: 'moderate',
},
'&:hover svg': {
color: 'link.primary.hover',
},
'&:active': {
transform: 'none',
},
},
'& div[role="radiogroup"]': {
'& > div': {
gap: '8px',
},
'& [class*="LargeRadio-module__root_"]': {
'& svg': {
transform: 'none',
},
'& label': {
'& p:not([data-size="sm"])': {
fontWeight: 'semibold !important',
},
'& p[data-size="sm"]': {
color: 'text.secondary',
},
},
'& > div[class*="LargeRadio-module__body_"]': {
border: 'none',
borderRadius: 'base',
backgroundColor: { _light: 'gray.50', _dark: 'whiteAlpha.100' },
},
'&:not([data-checked="true"]) input': {
backgroundColor: 'transparent',
border: '2px solid',
borderColor: 'input.border.filled',
},
'& > div > div:not([class*="_labelWrapper_"])': {
marginTop: '2px',
},
'& .tabler-icon-info-circle': {
stroke: 'text.primary',
width: '20px',
height: '20px',
},
},
},
'& [class*="TokenSelect-module__grid_"]': {
display: 'flex',
justifyContent: 'flex-start',
gap: '12px',
flexWrap: 'wrap',
},
'& [class*="TokenSelect-module__chainsList_"]': {
gap: 0,
marginTop: '24px',
'& > div': {
display: 'flex',
padding: '8px',
margin: '0 -8px',
borderRadius: 'base',
cursor: 'pointer',
'&:hover': {
backgroundColor: 'selected.control.bg',
},
'& > [class*="_chainRow_"]': {
flex: 1,
'&:hover': {
color: 'inherit',
},
},
'& > svg': {
width: '18px',
},
},
},
'& [class*="TokenSelect-module__stickySection_"]': {
top: '16px',
'& > div:last-child': {
gap: '8px !important',
},
},
'& [class*="TokenSelect-module__searchInput_"]': {
marginTop: '8px',
},
'& [class*="TokenSelect-module__tokensList_"]': {
gap: 0,
'& svg > path:first-child': {
stroke: 'text.primary',
},
},
'& [class*="TokenSelect-module__tokenRow_"]': {
padding: '8px',
margin: '0 -8px',
borderRadius: 'base',
'&:hover': {
backgroundColor: 'selected.control.bg',
},
'& [class*="_tokenIcon_"]': {
width: '40px',
height: '40px',
},
},
'& > .multisenderMantineProvider': {
'--mantine-color-text': 'text.primary',
'--mantine-color-red-light': 'colors.alert.bg.error',
'--mantine-color-blue-light': 'colors.alert.bg.info',
'--mantine-color-blue-light-color': 'colors.text.secondary',
'--mantine-color-blue-text': 'colors.hover',
'--mantine-color-blue-outline': 'colors.hover',
'--mantine-color-green-text': 'colors.green.500',
'& > div:nth-child(1)': {
padding: '0 !important',
gap: 0,
'& > :nth-child(1)': {
margin: '0 !important',
},
},
},
},
}}
>{ children }</Box>
);
const widgetConfig = Object.fromEntries(dappConfig?.chains.map((chainId) => {
const chainConfig = essentialDappsChainsConfig()?.chains.find((chain) => chain.id === chainId);
const explorerUrl = chainConfig?.app_config?.app?.baseUrl;
const apiUrl = chainConfig?.app_config?.apis?.general?.endpoint;
return [
chainId,
{
id: Number(chainId),
blockExplorerUrl: {
tx: `${ explorerUrl }/tx/`,
address: `${ explorerUrl }/address/`,
},
rpcUrls: [ `${ apiUrl }/api/eth-rpc` ],
blockScoutApiUrl: apiUrl,
},
];
}) || []);
const Multisend = () => {
const isMobile = useIsMobile();
return (
<>
<Container>
<MultisenderWidget
config={ widgetConfig }
logoType="minified"
posthogKey={ dappConfig?.posthogKey }
posthogHost={ dappConfig?.posthogHost }
classNames={{
theme: 'multisenderTheme',
mantineProvider: 'multisenderMantineProvider',
}}
/>
</Container>
{ (feature?.essentialDappsAdEnabled && !isMobile) && (
<AdBanner
format="desktop"
w="fit-content"
borderRadius="md"
overflow="hidden"
mx="auto"
mt={ 10 }
/>
) }
</>
);
};
export default Multisend;