Commit 720fc065 authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Add support for uniswap_v4 pools (#2748)

parent 5f0c3f5c
...@@ -15,7 +15,7 @@ NEXT_PUBLIC_API_BASE_PATH=/ ...@@ -15,7 +15,7 @@ NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=eth.blockscout.com NEXT_PUBLIC_API_HOST=eth.blockscout.com
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Revokescout','icon':'integration/partial','dappId':'revokescout'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}] NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Revokescout','icon':'integration/partial','dappId':'revokescout'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}]
NEXT_PUBLIC_DEX_POOLS_ENABLED=true NEXT_PUBLIC_DEX_POOLS_ENABLED=true
......
import type { Pool } from 'types/api/pools'; import type { Pool } from 'types/api/pools';
export const base: Pool = { export const base: Pool = {
contract_address: '0x06da0fd433c1a5d7a4faa01111c044910a184553', pool_id: '0x06da0fd433c1a5d7a4faa01111c044910a184553',
is_contract: true,
chain_id: '1', chain_id: '1',
base_token_address: '0xdac17f958d2ee523a2206206994597c13d831ec7', base_token_address: '0xdac17f958d2ee523a2206206994597c13d831ec7',
base_token_symbol: 'USDT', base_token_symbol: 'USDT',
......
export const POOL = { export const POOL = {
contract_address: '0x6a1041865b76d1dc33da0257582591227c57832c', pool_id: '0x6a1041865b76d1dc33da0257582591227c57832c',
is_contract: true,
chain_id: '1', chain_id: '1',
base_token_address: '0xf63e309818e4ea13782678ce6c31c1234fa61809', base_token_address: '0xf63e309818e4ea13782678ce6c31c1234fa61809',
base_token_symbol: 'JANET', base_token_symbol: 'JANET',
......
...@@ -7,7 +7,8 @@ export type PoolsResponse = { ...@@ -7,7 +7,8 @@ export type PoolsResponse = {
}; };
export type Pool = { export type Pool = {
contract_address: string; pool_id: string;
is_contract: boolean;
chain_id: string; chain_id: string;
base_token_address: string; base_token_address: string;
base_token_symbol: string; base_token_symbol: string;
......
...@@ -18,7 +18,7 @@ const hooksConfig = { ...@@ -18,7 +18,7 @@ const hooksConfig = {
test('base view +@mobile +@dark-mode', async({ render, mockApiResponse, mockTextAd, mockAssetResponse, page }) => { test('base view +@mobile +@dark-mode', async({ render, mockApiResponse, mockTextAd, mockAssetResponse, page }) => {
await mockTextAd(); await mockTextAd();
await mockApiResponse('contractInfo:pool', poolMock.base, { pathParams: { chainId: config.chain.id, hash: addressHash } }); await mockApiResponse('contractInfo:pool', poolMock.base, { pathParams: { chainId: config.chain.id, hash: addressHash } });
await mockApiResponse('general:address', addressMock.contract, { pathParams: { hash: poolMock.base.contract_address } }); await mockApiResponse('general:address', addressMock.contract, { pathParams: { hash: poolMock.base.pool_id } });
await mockAssetResponse(poolMock.base.quote_token_icon_url as string, './playwright/mocks/image_s.jpg'); await mockAssetResponse(poolMock.base.quote_token_icon_url as string, './playwright/mocks/image_s.jpg');
await mockAssetResponse(poolMock.base.base_token_icon_url as string, './playwright/mocks/image_md.jpg'); await mockAssetResponse(poolMock.base.base_token_icon_url as string, './playwright/mocks/image_md.jpg');
const component = await render(<Pool/>, { hooksConfig }); const component = await render(<Pool/>, { hooksConfig });
......
import { Box, Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -17,9 +17,11 @@ import { Skeleton } from 'toolkit/chakra/skeleton'; ...@@ -17,9 +17,11 @@ import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tag } from 'toolkit/chakra/tag'; import { Tag } from 'toolkit/chakra/tag';
import PoolInfo from 'ui/pool/PoolInfo'; import PoolInfo from 'ui/pool/PoolInfo';
import isCustomAppError from 'ui/shared/AppError/isCustomAppError'; import isCustomAppError from 'ui/shared/AppError/isCustomAppError';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import * as PoolEntity from 'ui/shared/entities/pool/PoolEntity'; import * as PoolEntity from 'ui/shared/entities/pool/PoolEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import InfoButton from 'ui/shared/InfoButton'; import InfoButton from 'ui/shared/InfoButton';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import VerifyWith from 'ui/shared/VerifyWith'; import VerifyWith from 'ui/shared/VerifyWith';
...@@ -38,9 +40,9 @@ const Pool = () => { ...@@ -38,9 +40,9 @@ const Pool = () => {
}); });
const addressQuery = useApiQuery('general:address', { const addressQuery = useApiQuery('general:address', {
pathParams: { hash: data?.contract_address }, pathParams: { hash: data?.pool_id },
queryOptions: { queryOptions: {
enabled: Boolean(data?.contract_address), enabled: Boolean(data?.is_contract),
placeholderData: addressStubs.ADDRESS_INFO, placeholderData: addressStubs.ADDRESS_INFO,
}, },
}); });
...@@ -81,10 +83,27 @@ const Pool = () => { ...@@ -81,10 +83,27 @@ const Pool = () => {
}); });
}, [ externalLinks ]); }, [ externalLinks ]);
const poolIdOrContract = React.useMemo(() => {
if (data?.is_contract && addressQuery.data) {
return <AddressEntity address={ addressQuery.data } isLoading={ addressQuery.isPlaceholderData }/>;
} else if (data?.pool_id) {
return (
<Skeleton loading={ isPlaceholderData } display="flex" alignItems="center" overflow="hidden">
<Flex overflow="hidden">
<HashStringShortenDynamic hash={ data?.pool_id }/>
</Flex>
<CopyToClipboard text={ data?.pool_id }/>
</Skeleton>
);
}
return null;
}, [ data, isPlaceholderData, addressQuery.isPlaceholderData, addressQuery.data ]);
const titleSecondRow = ( const titleSecondRow = (
<Flex alignItems="center" justifyContent="space-between" w="100%"> <Flex alignItems="center" justifyContent="space-between" w="100%">
{ addressQuery.data ? <AddressEntity address={ addressQuery.data } isLoading={ addressQuery.isPlaceholderData }/> : <Box/> } { poolIdOrContract }
<Flex gap={ 2 }> <Flex gap={ 2 } ml={ 2 }>
<InfoButton> <InfoButton>
{ `This Liquidity Provider (LP) token represents ${ data?.base_token_symbol }/${ data?.quote_token_symbol } pairing.` } { `This Liquidity Provider (LP) token represents ${ data?.base_token_symbol }/${ data?.quote_token_symbol } pairing.` }
</InfoButton> </InfoButton>
......
...@@ -42,7 +42,7 @@ const Pools = () => { ...@@ -42,7 +42,7 @@ const Pools = () => {
<Box hideFrom="lg"> <Box hideFrom="lg">
{ poolsQuery.data?.items.map((item, index) => ( { poolsQuery.data?.items.map((item, index) => (
<PoolsListItem <PoolsListItem
key={ item.contract_address + (poolsQuery.isPlaceholderData ? index : '') } key={ item.pool_id + (poolsQuery.isPlaceholderData ? index : '') }
isLoading={ poolsQuery.isPlaceholderData } isLoading={ poolsQuery.isPlaceholderData }
item={ item } item={ item }
/> />
......
...@@ -6,8 +6,10 @@ import getPoolLinks from 'lib/pools/getPoolLinks'; ...@@ -6,8 +6,10 @@ import getPoolLinks from 'lib/pools/getPoolLinks';
import { Image } from 'toolkit/chakra/image'; import { Image } from 'toolkit/chakra/image';
import { Link } from 'toolkit/chakra/link'; import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton'; import { Skeleton } from 'toolkit/chakra/skeleton';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import PoolEntity from 'ui/shared/entities/pool/PoolEntity'; import PoolEntity from 'ui/shared/entities/pool/PoolEntity';
import HashStringShorten from 'ui/shared/HashStringShorten';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = { type Props = {
...@@ -15,7 +17,7 @@ type Props = { ...@@ -15,7 +17,7 @@ type Props = {
isLoading?: boolean; isLoading?: boolean;
}; };
const UserOpsListItem = ({ item, isLoading }: Props) => { const PoolsListItem = ({ item, isLoading }: Props) => {
const externalLinks = getPoolLinks(item); const externalLinks = getPoolLinks(item);
return ( return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto"> <ListItemMobileGrid.Container gridTemplateColumns="100px auto">
...@@ -25,10 +27,24 @@ const UserOpsListItem = ({ item, isLoading }: Props) => { ...@@ -25,10 +27,24 @@ const UserOpsListItem = ({ item, isLoading }: Props) => {
<PoolEntity pool={ item } fontWeight={ 700 } isLoading={ isLoading }/> <PoolEntity pool={ item } fontWeight={ 700 } isLoading={ isLoading }/>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
{ item.is_contract && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>Contract</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Contract</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<AddressEntity address={{ hash: item.contract_address }} noIcon linkVariant="secondary" isLoading={ isLoading }/> <AddressEntity address={{ hash: item.pool_id }} noIcon linkVariant="secondary" isLoading={ isLoading } truncation="constant_long"/>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
</>
) }
{ !item.is_contract && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>Pool ID</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<HashStringShorten hash={ item.pool_id } type="long"/>
<CopyToClipboard text={ item.pool_id }/>
</ListItemMobileGrid.Value>
</>
) }
<ListItemMobileGrid.Label isLoading={ isLoading }>Liquidity</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Liquidity</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
...@@ -52,4 +68,4 @@ const UserOpsListItem = ({ item, isLoading }: Props) => { ...@@ -52,4 +68,4 @@ const UserOpsListItem = ({ item, isLoading }: Props) => {
); );
}; };
export default UserOpsListItem; export default PoolsListItem;
...@@ -27,7 +27,7 @@ const PoolsTable = ({ items, page, isLoading, top }: Props) => { ...@@ -27,7 +27,7 @@ const PoolsTable = ({ items, page, isLoading, top }: Props) => {
</TableHeaderSticky> </TableHeaderSticky>
<TableBody> <TableBody>
{ items.map((item, index) => ( { items.map((item, index) => (
<PoolsTableItem key={ item.contract_address + (isLoading ? index : '') } item={ item } index={ index } page={ page } isLoading={ isLoading }/> <PoolsTableItem key={ item.pool_id + (isLoading ? index : '') } item={ item } index={ index } page={ page } isLoading={ isLoading }/>
)) } )) }
</TableBody> </TableBody>
</TableRoot> </TableRoot>
......
...@@ -10,8 +10,10 @@ import { Link } from 'toolkit/chakra/link'; ...@@ -10,8 +10,10 @@ import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton'; import { Skeleton } from 'toolkit/chakra/skeleton';
import { TableCell, TableRow } from 'toolkit/chakra/table'; import { TableCell, TableRow } from 'toolkit/chakra/table';
import { Tooltip } from 'toolkit/chakra/tooltip'; import { Tooltip } from 'toolkit/chakra/tooltip';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import PoolEntity from 'ui/shared/entities/pool/PoolEntity'; import PoolEntity from 'ui/shared/entities/pool/PoolEntity';
import HashStringShorten from 'ui/shared/HashStringShorten';
type Props = { type Props = {
item: Pool; item: Pool;
...@@ -37,13 +39,20 @@ const PoolsTableItem = ({ ...@@ -37,13 +39,20 @@ const PoolsTableItem = ({
</Skeleton> </Skeleton>
<Box overflow="hidden"> <Box overflow="hidden">
<PoolEntity pool={ item } fontWeight={ 700 } mb={ 2 } isLoading={ isLoading }/> <PoolEntity pool={ item } fontWeight={ 700 } mb={ 2 } isLoading={ isLoading }/>
{ item.is_contract ? (
<AddressEntity <AddressEntity
address={{ hash: item.contract_address }} address={{ hash: item.pool_id }}
noIcon noIcon
isLoading={ isLoading } isLoading={ isLoading }
truncation="constant_long" truncation="constant_long"
linkVariant="secondary" linkVariant="secondary"
/> />
) : (
<Flex color="text.secondary" alignItems="center">
<HashStringShorten hash={ item.pool_id } type="long"/>
<CopyToClipboard text={ item.pool_id }/>
</Flex>
) }
</Box> </Box>
</Flex> </Flex>
</TableCell> </TableCell>
......
...@@ -16,7 +16,7 @@ import * as TokenEntity from '../token/TokenEntity'; ...@@ -16,7 +16,7 @@ import * as TokenEntity from '../token/TokenEntity';
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'pool'>; type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'pool'>;
const Link = chakra((props: LinkProps) => { const Link = chakra((props: LinkProps) => {
const defaultHref = route({ pathname: '/pools/[hash]', query: { hash: props.pool.contract_address } }); const defaultHref = route({ pathname: '/pools/[hash]', query: { hash: props.pool.pool_id } });
return ( return (
<EntityBase.Link <EntityBase.Link
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment