Commit 52bcc45d authored by tom's avatar tom

sort ERC-20 tokens

parent 25ddae54
...@@ -2,17 +2,16 @@ import { Flex, Text, useColorModeValue } from '@chakra-ui/react'; ...@@ -2,17 +2,16 @@ import { Flex, Text, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import type { EnhancedData } from './utils';
interface Props { interface Props {
data: AddressTokenBalance; data: EnhancedData;
usd?: string;
} }
const TokenItem = ({ data, usd }: Props) => { const TokenItem = ({ data }: Props) => {
const secondRow = (() => { const secondRow = (() => {
switch (data.token.type) { switch (data.token.type) {
...@@ -20,7 +19,7 @@ const TokenItem = ({ data, usd }: Props) => { ...@@ -20,7 +19,7 @@ const TokenItem = ({ data, usd }: Props) => {
const tokenDecimals = Number(data.token.decimals) || 18; const tokenDecimals = Number(data.token.decimals) || 18;
return ( return (
<> <>
<Text >{ BigNumber(data.value).dividedBy(10 ** tokenDecimals).dp(2).toFormat() } { data.token.symbol }</Text> <Text >{ BigNumber(data.value).dividedBy(10 ** tokenDecimals).toFormat(2) } { data.token.symbol }</Text>
{ data.token.exchange_rate && <Text >@{ data.token.exchange_rate }</Text> } { data.token.exchange_rate && <Text >@{ data.token.exchange_rate }</Text> }
</> </>
); );
...@@ -57,7 +56,7 @@ const TokenItem = ({ data, usd }: Props) => { ...@@ -57,7 +56,7 @@ const TokenItem = ({ data, usd }: Props) => {
<Flex alignItems="center" w="100%"> <Flex alignItems="center" w="100%">
<TokenLogo hash={ data.token.address } name={ data.token.name } boxSize={ 6 }/> <TokenLogo hash={ data.token.address } name={ data.token.name } boxSize={ 6 }/>
<Text fontWeight={ 700 } ml={ 2 }>{ data.token.name || <HashStringShorten hash={ data.token.address }/> }</Text> <Text fontWeight={ 700 } ml={ 2 }>{ data.token.name || <HashStringShorten hash={ data.token.address }/> }</Text>
{ usd && <Text fontWeight={ 700 } ml="auto">${ usd }</Text> } { data.usd && <Text fontWeight={ 700 } ml="auto">${ data.usd.toFormat(2) }</Text> }
</Flex> </Flex>
<Flex alignItems="center" justifyContent="space-between" w="100%"> <Flex alignItems="center" justifyContent="space-between" w="100%">
{ secondRow } { secondRow }
......
...@@ -18,51 +18,17 @@ import searchIcon from 'icons/search.svg'; ...@@ -18,51 +18,17 @@ import searchIcon from 'icons/search.svg';
import TokenItem from './TokenItem'; import TokenItem from './TokenItem';
import TokensButton from './TokensButton'; import TokensButton from './TokensButton';
import type { Sort } from './utils';
type Sort = 'desc' | 'asc'; import { SORTABLE_TOKENS, sortTokenGroups, sortingFns, calculateUsdValue, filterTokens } from './utils';
const SORTABLE_TOKENS: Array<TokenType> = [ 'ERC-20', 'ERC-1155' ];
const TOKEN_GROUPS_ORDER: Array<TokenType> = [ 'ERC-20', 'ERC-721', 'ERC-1155' ];
type TokenGroup = [string, Array<AddressTokenBalance>];
const sortTokenGroups = (groupA: TokenGroup, groupB: TokenGroup) => {
return TOKEN_GROUPS_ORDER.indexOf(groupA[0] as TokenType) > TOKEN_GROUPS_ORDER.indexOf(groupB[0] as TokenType) ? 1 : -1;
};
const sortErc1155Tokens = (sort: 'desc' | 'asc') => (dataA: AddressTokenBalance, dataB: AddressTokenBalance) => {
if (dataA.value === dataB.value) {
return 0;
}
if (sort === 'desc') {
return Number(dataA.value) > Number(dataB.value) ? -1 : 1;
}
return Number(dataA.value) > Number(dataB.value) ? 1 : -1;
};
const sortErc20Tokens = () => () => 0;
const sortErc721Tokens = () => () => 0;
const sortingFns = {
'ERC-20': sortErc20Tokens,
'ERC-721': sortErc721Tokens,
'ERC-1155': sortErc1155Tokens,
};
const filterTokens = (searchTerm: string) => ({ token }: AddressTokenBalance) => {
if (!token.name) {
return !searchTerm ? true : token.address.toLowerCase().includes(searchTerm);
}
return token.name?.toLowerCase().includes(searchTerm);
};
interface Props { interface Props {
data: Array<AddressTokenBalance>; data: Array<AddressTokenBalance>;
} }
const AddressTokenSelect = ({ data }: Props) => { const Tokens = ({ data }: Props) => {
const [ searchTerm, setSearchTerm ] = React.useState(''); const [ searchTerm, setSearchTerm ] = React.useState('');
const [ erc1155sort, setErc1155Sort ] = React.useState<Sort>('desc'); const [ erc1155sort, setErc1155Sort ] = React.useState<Sort>('desc');
const [ erc20sort, setErc20Sort ] = React.useState<Sort>('desc');
const { isOpen, onToggle, onClose } = useDisclosure(); const { isOpen, onToggle, onClose } = useDisclosure();
const handleInputChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleInputChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
...@@ -74,19 +40,22 @@ const AddressTokenSelect = ({ data }: Props) => { ...@@ -74,19 +40,22 @@ const AddressTokenSelect = ({ data }: Props) => {
if (tokenType === 'ERC-1155') { if (tokenType === 'ERC-1155') {
setErc1155Sort((prevValue) => prevValue === 'desc' ? 'asc' : 'desc'); setErc1155Sort((prevValue) => prevValue === 'desc' ? 'asc' : 'desc');
} }
if (tokenType === 'ERC-20') {
setErc20Sort((prevValue) => prevValue === 'desc' ? 'asc' : 'desc');
}
}, []); }, []);
const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600'); const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200'); const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200');
const bgColor = useColorModeValue('white', 'gray.900'); const bgColor = useColorModeValue('white', 'gray.900');
const filteredData = data.filter(filterTokens(searchTerm.toLowerCase())); const modifiedData = data.filter(filterTokens(searchTerm.toLowerCase())).map(calculateUsdValue);
const groupedData = _groupBy(filteredData, 'token.type'); const groupedData = _groupBy(modifiedData, 'token.type');
return ( return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy> <Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger> <PopoverTrigger>
<TokensButton isOpen={ isOpen } onClick={ onToggle } data={ data }/> <TokensButton isOpen={ isOpen } onClick={ onToggle } data={ modifiedData }/>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent w="355px" maxH="450px" overflowY="scroll"> <PopoverContent w="355px" maxH="450px" overflowY="scroll">
<PopoverBody px={ 4 } py={ 6 } bgColor={ bgColor } boxShadow="2xl" > <PopoverBody px={ 4 } py={ 6 } bgColor={ bgColor } boxShadow="2xl" >
...@@ -105,12 +74,15 @@ const AddressTokenSelect = ({ data }: Props) => { ...@@ -105,12 +74,15 @@ const AddressTokenSelect = ({ data }: Props) => {
<Flex flexDir="column" rowGap={ 6 }> <Flex flexDir="column" rowGap={ 6 }>
{ Object.entries(groupedData).sort(sortTokenGroups).map(([ tokenType, tokenInfo ]) => { { Object.entries(groupedData).sort(sortTokenGroups).map(([ tokenType, tokenInfo ]) => {
const type = tokenType as TokenType; const type = tokenType as TokenType;
const arrowTransform = type === 'ERC-1155' && erc1155sort === 'desc' ? 'rotate(90deg)' : 'rotate(-90deg)'; const arrowTransform = (type === 'ERC-1155' && erc1155sort === 'desc') || (type === 'ERC-20' && erc20sort === 'desc') ?
'rotate(90deg)' :
'rotate(-90deg)';
const sortDirection: Sort = (() => { const sortDirection: Sort = (() => {
switch (type) { switch (type) {
case 'ERC-1155': case 'ERC-1155':
return erc1155sort; return erc1155sort;
case 'ERC-20':
return erc20sort;
default: default:
return 'desc'; return 'desc';
} }
...@@ -126,16 +98,16 @@ const AddressTokenSelect = ({ data }: Props) => { ...@@ -126,16 +98,16 @@ const AddressTokenSelect = ({ data }: Props) => {
</Link> </Link>
) } ) }
</Flex> </Flex>
{ tokenInfo.sort(sortingFns[type](sortDirection)).map((data) => <TokenItem key={ data.token.address } data={ data }/>) } { tokenInfo.sort(sortingFns[type](sortDirection)).map((data) => <TokenItem key={ data.token.address + data.token_id } data={ data }/>) }
</Box> </Box>
); );
}) } }) }
</Flex> </Flex>
{ filteredData.length === 0 && searchTerm && <Text fontSize="sm">Could not find any matches.</Text> } { modifiedData.length === 0 && searchTerm && <Text fontSize="sm">Could not find any matches.</Text> }
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
); );
}; };
export default React.memo(AddressTokenSelect); export default React.memo(Tokens);
...@@ -2,26 +2,20 @@ import { Button, Icon, Text } from '@chakra-ui/react'; ...@@ -2,26 +2,20 @@ import { Button, Icon, Text } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import arrowIcon from 'icons/arrows/east-mini.svg'; import arrowIcon from 'icons/arrows/east-mini.svg';
import tokensIcon from 'icons/tokens.svg'; import tokensIcon from 'icons/tokens.svg';
import { ZERO } from 'lib/consts'; import { ZERO } from 'lib/consts';
import type { EnhancedData } from './utils';
interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
onClick: () => void; onClick: () => void;
data: Array<AddressTokenBalance>; data: Array<EnhancedData>;
} }
const TokensButton = ({ isOpen, onClick, data }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => { const TokensButton = ({ isOpen, onClick, data }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
const usdBn = data.reduce((result, item) => { const totalBn = data.reduce((result, item) => !item.usd ? result : result.plus(BigNumber(item.usd)), ZERO);
if (!item.token.exchange_rate) {
return result;
}
const decimals = Number(item.token.decimals || '18');
return BigNumber(item.value).div(BigNumber(10 ** decimals)).multipliedBy(BigNumber(item.token.exchange_rate));
}, ZERO);
return ( return (
<Button <Button
...@@ -33,7 +27,7 @@ const TokensButton = ({ isOpen, onClick, data }: Props, ref: React.ForwardedRef< ...@@ -33,7 +27,7 @@ const TokensButton = ({ isOpen, onClick, data }: Props, ref: React.ForwardedRef<
> >
<Icon as={ tokensIcon } boxSize={ 4 } mr={ 2 }/> <Icon as={ tokensIcon } boxSize={ 4 } mr={ 2 }/>
<Text fontWeight={ 600 }>{ data.length }</Text> <Text fontWeight={ 600 }>{ data.length }</Text>
<Text whiteSpace="pre" variant="secondary" fontWeight={ 400 }> (${ usdBn.toFixed(2) })</Text> <Text whiteSpace="pre" variant="secondary" fontWeight={ 400 }> (${ totalBn.toFormat(2) })</Text>
<Icon as={ arrowIcon } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="normal" boxSize={ 5 } ml={ 3 }/> <Icon as={ arrowIcon } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="normal" boxSize={ 5 } ml={ 3 }/>
</Button> </Button>
); );
......
import BigNumber from 'bignumber.js';
import type { AddressTokenBalance } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo';
export type EnhancedData = AddressTokenBalance & {
usd?: BigNumber ;
}
export type Sort = 'desc' | 'asc';
export const SORTABLE_TOKENS: Array<TokenType> = [ 'ERC-20', 'ERC-1155' ];
const TOKEN_GROUPS_ORDER: Array<TokenType> = [ 'ERC-20', 'ERC-721', 'ERC-1155' ];
type TokenGroup = [string, Array<AddressTokenBalance>];
export const sortTokenGroups = (groupA: TokenGroup, groupB: TokenGroup) => {
return TOKEN_GROUPS_ORDER.indexOf(groupA[0] as TokenType) > TOKEN_GROUPS_ORDER.indexOf(groupB[0] as TokenType) ? 1 : -1;
};
const sortErc1155Tokens = (sort: Sort) => (dataA: AddressTokenBalance, dataB: AddressTokenBalance) => {
if (dataA.value === dataB.value) {
return 0;
}
if (sort === 'desc') {
return Number(dataA.value) > Number(dataB.value) ? -1 : 1;
}
return Number(dataA.value) > Number(dataB.value) ? 1 : -1;
};
const sortErc20Tokens = (sort: Sort) => (dataA: EnhancedData, dataB: EnhancedData) => {
// keep tokens without usd value in the end of the group
if (!dataB.usd) {
return -1;
}
if (!dataA.usd) {
return 0;
}
if (sort === 'desc') {
return dataA.usd.gt(dataB.usd) ? -1 : 1;
}
return dataA.usd.gt(dataB.usd) ? 1 : -1;
};
const sortErc721Tokens = () => () => 0;
export const sortingFns = {
'ERC-20': sortErc20Tokens,
'ERC-721': sortErc721Tokens,
'ERC-1155': sortErc1155Tokens,
};
export const filterTokens = (searchTerm: string) => ({ token }: AddressTokenBalance) => {
if (!token.name) {
return !searchTerm ? true : token.address.toLowerCase().includes(searchTerm);
}
return token.name?.toLowerCase().includes(searchTerm);
};
export const calculateUsdValue = (data: AddressTokenBalance): EnhancedData => {
if (data.token.type !== 'ERC-20') {
return data;
}
const exchangeRate = data.token.exchange_rate;
if (!exchangeRate) {
return data;
}
const decimals = Number(data.token.decimals || '18');
return {
...data,
usd: BigNumber(data.value).div(BigNumber(10 ** decimals)).multipliedBy(BigNumber(exchangeRate)),
};
};
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