diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml index 759b120b17..a8ed3ee27a 100644 --- a/.github/workflows/cleanup.yml +++ b/.github/workflows/cleanup.yml @@ -29,5 +29,5 @@ jobs: cleanup_docker_image: uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup_docker.yaml@master with: - dockerImage: prerelease-$GITHUB_REF_NAME_SLUG + dockerImage: review-$GITHUB_REF_NAME_SLUG secrets: inherit diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 95b6bb2310..cd1be4e2b7 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -318,6 +318,7 @@ "main.L2", "poa_core", "eth_goerli", + "sepolia", "eth", "rootstock", "polygon", diff --git a/configs/envs/.env.sepolia b/configs/envs/.env.sepolia new file mode 100644 index 0000000000..b54adc0775 --- /dev/null +++ b/configs/envs/.env.sepolia @@ -0,0 +1,60 @@ +# Set of ENVs for Sepolia testnet network explorer +# https://eth-sepolia.blockscout.com/ + +# app configuration +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 + +# blockchain parameters +NEXT_PUBLIC_NETWORK_NAME=Sepolia +NEXT_PUBLIC_NETWORK_SHORT_NAME=Sepolia +NEXT_PUBLIC_NETWORK_ID=11155111 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_NETWORK_RPC_URL=https://eth-sepolia.public.blastapi.io +NEXT_PUBLIC_IS_TESTNET=true + +# api configuration +NEXT_PUBLIC_API_HOST=eth-sepolia.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ + +# ui config +## homepage +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='rgba(51, 53, 67, 1)' +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='rgba(165, 252, 122, 1)' +## sidebar +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-sepolia.json +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png +NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://sepolia.drpc.org?ref=559183','text':'Public RPC'}] +## footer +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/sepolia.json +##views +NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'LooksRare','collection_url':'https://sepolia.looksrare.org/collections/{hash}','instance_url':'https://sepolia.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}] +## misc +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','baseUrl':'https://sepolia.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}},{'title':'Tenderly','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/sepolia'}}] + +# app features +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xbf69c7abc4fee283b59a9633dadfdaedde5c5ee0fba3e80a08b5b8a3acbd4363 +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_AUTH_URL=http://localhost:3000 +NEXT_PUBLIC_LOGOUT_URL=https://blockscout-goerli.us.auth0.com/v2/logout +NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C +NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-dev.blockscout.com +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask'] +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_HAS_BEACON_CHAIN=true + +#meta +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/sepolia-testnet.png diff --git a/pages/apps/index.tsx b/pages/apps/index.tsx index 5b95867329..17f75586ac 100644 --- a/pages/apps/index.tsx +++ b/pages/apps/index.tsx @@ -4,15 +4,26 @@ import React from 'react'; import PageNextJs from 'nextjs/PageNextJs'; +import config from 'configs/app'; +import LinkExternal from 'ui/shared/LinkExternal'; import PageTitle from 'ui/shared/Page/PageTitle'; +const feature = config.features.marketplace; + const Marketplace = dynamic(() => import('ui/pages/Marketplace'), { ssr: false }); const Page: NextPage = () => { return ( <> - + + Submit app + + ) } + /> diff --git a/ui/address/contract/ContractCode.pw.tsx b/ui/address/contract/ContractCode.pw.tsx index ef4234d907..52c7fc9682 100644 --- a/ui/address/contract/ContractCode.pw.tsx +++ b/ui/address/contract/ContractCode.pw.tsx @@ -78,6 +78,31 @@ test('verified with changed byte code socket', async({ mount, page, createSocket await expect(component).toHaveScreenshot(); }); +test('verified via lookup in eth_bytecode_db', async({ mount, page, createSocket }) => { + await page.route(CONTRACT_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(contractMock.nonVerified), + })); + await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort()); + + await mount( + + + , + { hooksConfig }, + ); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, 'addresses:' + addressHash.toLowerCase()); + + await page.waitForResponse(CONTRACT_API_URL); + socketServer.sendMessage(socket, channel, 'smart_contract_was_verified', {}); + + const request = await page.waitForRequest(CONTRACT_API_URL); + + expect(request).toBeTruthy(); +}); + test('verified with multiple sources', async({ mount, page }) => { await page.route(CONTRACT_API_URL, (route) => route.fulfill({ status: 200, diff --git a/ui/address/contract/ContractCode.tsx b/ui/address/contract/ContractCode.tsx index 465602befd..cfa2e5f003 100644 --- a/ui/address/contract/ContractCode.tsx +++ b/ui/address/contract/ContractCode.tsx @@ -38,7 +38,6 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState(); const queryClient = useQueryClient(); - const refetchQueries = queryClient.refetchQueries; const addressInfo = queryClient.getQueryData(getResourceKey('address', { pathParams: { hash: addressHash } })); const { data, isPlaceholderData, isError } = useApiQuery('contract', { @@ -55,13 +54,13 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { }, [ ]); const handleContractWasVerifiedMessage: SocketMessage.SmartContractWasVerified['handler'] = React.useCallback(() => { - refetchQueries({ + queryClient.refetchQueries({ queryKey: getResourceKey('address', { pathParams: { hash: addressHash } }), }); - refetchQueries({ + queryClient.refetchQueries({ queryKey: getResourceKey('contract', { pathParams: { hash: addressHash } }), }); - }, [ addressHash, refetchQueries ]); + }, [ addressHash, queryClient ]); const enableQuery = React.useCallback(() => setIsQueryEnabled(true), []); diff --git a/ui/address/tokens/ERC20TokensTableItem.tsx b/ui/address/tokens/ERC20TokensTableItem.tsx index be7ade69e4..ed78abcfd5 100644 --- a/ui/address/tokens/ERC20TokensTableItem.tsx +++ b/ui/address/tokens/ERC20TokensTableItem.tsx @@ -22,7 +22,13 @@ const ERC20TokensTableItem = ({ } = getCurrencyValue({ value: value, exchangeRate: token.exchange_rate, decimals: token.decimals, accuracy: 8, accuracyUsd: 2 }); return ( - + - + diff --git a/ui/pages/Marketplace.tsx b/ui/pages/Marketplace.tsx index 25b5370a9e..7024498b39 100644 --- a/ui/pages/Marketplace.tsx +++ b/ui/pages/Marketplace.tsx @@ -1,4 +1,4 @@ -import { Box, Link, Skeleton } from '@chakra-ui/react'; +import { Box } from '@chakra-ui/react'; import React from 'react'; import config from 'configs/app'; @@ -7,7 +7,6 @@ import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu' import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal'; import MarketplaceList from 'ui/marketplace/MarketplaceList'; import FilterInput from 'ui/shared/filters/FilterInput'; -import IconSvg from 'ui/shared/IconSvg'; import useMarketplace from '../marketplace/useMarketplace'; const feature = config.features.marketplace; @@ -91,29 +90,6 @@ const Marketplace = () => { appId={ selectedApp.id } /> ) } - - - - - - Submit an app - - ); }; diff --git a/ui/shared/address/AddressAddToWallet.tsx b/ui/shared/address/AddressAddToWallet.tsx index 119b4419ec..144e21101f 100644 --- a/ui/shared/address/AddressAddToWallet.tsx +++ b/ui/shared/address/AddressAddToWallet.tsx @@ -107,7 +107,7 @@ const AddressAddToWallet = ({ className, token, isLoading, variant = 'icon', ico return ( - + diff --git a/ui/shared/layout/LayoutApp.tsx b/ui/shared/layout/LayoutApp.tsx index 469ef3b536..e2a169de9e 100644 --- a/ui/shared/layout/LayoutApp.tsx +++ b/ui/shared/layout/LayoutApp.tsx @@ -12,10 +12,11 @@ import * as Layout from './components'; const LayoutDefault = ({ children }: Props) => { return ( + diff --git a/ui/snippets/profileMenu/ProfileMenuDesktop.tsx b/ui/snippets/profileMenu/ProfileMenuDesktop.tsx index 9592758040..90e445ec47 100644 --- a/ui/snippets/profileMenu/ProfileMenuDesktop.tsx +++ b/ui/snippets/profileMenu/ProfileMenuDesktop.tsx @@ -76,7 +76,7 @@ const ProfileMenuDesktop = ({ isHomePage }: Props) => { textAlign="center" padding={ 2 } isDisabled={ hasMenu } - openDelay={ 300 } + openDelay={ 500 } > diff --git a/ui/snippets/searchBar/SearchBar.tsx b/ui/snippets/searchBar/SearchBar.tsx index 1e1dfadb35..f8b07afe1e 100644 --- a/ui/snippets/searchBar/SearchBar.tsx +++ b/ui/snippets/searchBar/SearchBar.tsx @@ -1,7 +1,7 @@ -import { Box, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure, PopoverFooter } from '@chakra-ui/react'; +import { Box, Portal, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure, PopoverFooter, useOutsideClick } from '@chakra-ui/react'; import _debounce from 'lodash/debounce'; import { useRouter } from 'next/router'; -import type { FormEvent, FocusEvent } from 'react'; +import type { FormEvent } from 'react'; import React from 'react'; import { Element } from 'react-scroll'; @@ -59,13 +59,15 @@ const SearchBar = ({ isHomepage }: Props) => { inputRef.current?.querySelector('input')?.blur(); }, [ onClose ]); - const handleBlur = React.useCallback((event: FocusEvent) => { - const isFocusInMenu = menuRef.current?.contains(event.relatedTarget); - const isFocusInInput = inputRef.current?.contains(event.relatedTarget); - if (!isFocusInMenu && !isFocusInInput) { - onClose(); + const handleOutsideClick = React.useCallback((event: Event) => { + const isFocusInInput = inputRef.current?.contains(event.target as Node); + + if (!isFocusInInput) { + handelHide(); } - }, [ onClose ]); + }, [ handelHide ]); + + useOutsideClick({ ref: menuRef, handler: handleOutsideClick }); const handleClear = React.useCallback(() => { handleSearchTermChange(''); @@ -118,53 +120,54 @@ const SearchBar = ({ isHomepage }: Props) => { onChange={ handleSearchTermChange } onSubmit={ handleSubmit } onFocus={ handleFocus } - onBlur={ handleBlur } onHide={ handelHide } onClear={ handleClear } isHomepage={ isHomepage } value={ searchTerm } /> - - + - - { searchTerm.trim().length === 0 && recentSearchKeywords.length > 0 && ( - - ) } - { searchTerm.trim().length > 0 && ( - - ) } - - - { searchTerm.trim().length > 0 && query.data && query.data.length >= 50 && ( - - + { searchTerm.trim().length === 0 && recentSearchKeywords.length > 0 && ( + + ) } + { searchTerm.trim().length > 0 && ( + + ) } + + + { searchTerm.trim().length > 0 && query.data && query.data.length >= 50 && ( + + View all results - - - ) } - + + + ) } + + ); }; diff --git a/ui/snippets/searchBar/SearchBarInput.tsx b/ui/snippets/searchBar/SearchBarInput.tsx index a7ea26d2e7..c8c9d30265 100644 --- a/ui/snippets/searchBar/SearchBarInput.tsx +++ b/ui/snippets/searchBar/SearchBarInput.tsx @@ -20,25 +20,33 @@ interface Props { } const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, onHide, onClear, value }: Props, ref: React.ForwardedRef) => { + const innerRef = React.useRef(null); + React.useImperativeHandle(ref, () => innerRef.current as HTMLFormElement, []); const [ isSticky, setIsSticky ] = React.useState(false); const scrollDirection = useScrollDirection(); const isMobile = useIsMobile(); const handleScroll = React.useCallback(() => { const TOP_BAR_HEIGHT = 36; - if (window.pageYOffset >= TOP_BAR_HEIGHT) { - setIsSticky(true); - } else { - setIsSticky(false); + if (!isHomepage) { + if (window.scrollY >= TOP_BAR_HEIGHT) { + setIsSticky(true); + } else { + setIsSticky(false); + } } - }, [ ]); + const clientRect = isMobile && innerRef?.current?.getBoundingClientRect(); + if (clientRect && clientRect.y < TOP_BAR_HEIGHT) { + onHide?.(); + } + }, [ isMobile, onHide, isHomepage ]); const handleChange = React.useCallback((event: ChangeEvent) => { onChange(event.target.value); }, [ onChange ]); React.useEffect(() => { - if (!isMobile || isHomepage) { + if (!isMobile) { return; } const throttledHandleScroll = throttle(handleScroll, 300); @@ -48,22 +56,14 @@ const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, onHid return () => { window.removeEventListener('scroll', throttledHandleScroll); }; - // replicate componentDidMount - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ isMobile ]); + }, [ isMobile, handleScroll ]); const bgColor = useColorModeValue('white', 'black'); const transformMobile = scrollDirection !== 'down' ? 'translateY(0)' : 'translateY(-100%)'; - React.useEffect(() => { - if (isMobile && scrollDirection === 'down') { - onHide?.(); - } - }, [ scrollDirection, onHide, isMobile ]); - return ( { const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen } = useWallet(); const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors(); const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean(false); + const isMobile = useIsMobile(); const variant = React.useMemo(() => { if (isWalletConnected) { @@ -55,7 +57,7 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => { isOpen={ isPopoverOpen } onClose={ setIsPopoverOpen.off } > - +