diff --git a/src/components/Modal/common/TokenSelector.tsx b/src/components/Modal/common/TokenSelector.tsx index a4939c68b..7932ea297 100644 --- a/src/components/Modal/common/TokenSelector.tsx +++ b/src/components/Modal/common/TokenSelector.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { ListRenderItemInfo } from 'react-native'; import i18n from 'utils/i18n/i18n'; import { FullSizeSelectModal } from 'components/common/SelectModal'; @@ -8,12 +8,20 @@ import { EmptyList } from 'components/EmptyList'; import { MagnifyingGlass } from 'phosphor-react-native'; import { useNavigation } from '@react-navigation/native'; import { RootNavigationProps } from 'routes/index'; +import BigN from 'bignumber.js'; +import { useSelector } from 'react-redux'; +import { RootState } from 'stores/index'; +import { BN_ZERO } from 'utils/chainBalances'; +import { getInputValuesFromString } from 'screens/Transaction/SendFund/Amount'; export type TokenItemType = { name: string; slug: string; symbol: string; originChain: string; + free?: BigN; + price?: number; + decimals?: number; }; interface Props { @@ -34,6 +42,29 @@ interface Props { showAddBtn?: boolean; } +const convertChainActivePriority = (active?: boolean) => (active ? 1 : 0); + +const convertTokenBalance = (a: TokenItemType, b: TokenItemType) => { + const aFree = new BigN(a.free || BN_ZERO); + const bFree = new BigN(b.free || BN_ZERO); + const aResult = new BigN(a.free || BN_ZERO).multipliedBy(new BigN(a.price || BN_ZERO)); + const bResult = new BigN(b.free || BN_ZERO).multipliedBy(new BigN(b.price || BN_ZERO)); + + if (aResult.eq(bResult)) { + return Number(getInputValuesFromString(aFree.toFixed(), a.decimals || 0)) - + Number(getInputValuesFromString(bFree.toFixed(), b.decimals || 0)) > + 0 + ? -1 + : 1; + } + + return Number(getInputValuesFromString(aResult.toFixed(), a.decimals || 0)) - + Number(getInputValuesFromString(bResult.toFixed(), b.decimals || 0)) > + 0 + ? -1 + : 1; +}; + export const TokenSelector = ({ items, selectedValueMap, @@ -52,17 +83,27 @@ export const TokenSelector = ({ showAddBtn = true, }: Props) => { const navigation = useNavigation(); + const { chainStateMap } = useSelector((state: RootState) => state.chainStore); useEffect(() => { setAdjustPan(); }, []); + const filteredItems = useMemo((): TokenItemType[] => { + return items.sort((a, b) => { + return ( + convertChainActivePriority(chainStateMap[b.originChain]?.active) - + convertChainActivePriority(chainStateMap[a.originChain]?.active) || convertTokenBalance(a, b) + ); + }); + }, [chainStateMap, items]); + const _onSelectItem = (item: TokenItemType) => { onSelectItem && onSelectItem(item); }; return ( tokenSelectorRef?.current?.onCloseModal()} selectModalType={'single'} diff --git a/src/components/TokenSelectItem.tsx b/src/components/TokenSelectItem.tsx index 762d467d7..dae69f85c 100644 --- a/src/components/TokenSelectItem.tsx +++ b/src/components/TokenSelectItem.tsx @@ -5,9 +5,11 @@ import Text from 'components/Text'; import { ColorMap } from 'styles/color'; import { FontMedium, FontSemiBold } from 'styles/sharedStyles'; import { CheckCircle } from 'phosphor-react-native'; -import { Icon, Typography } from 'components/design-system-ui'; +import { Icon, Typography, Number } from 'components/design-system-ui'; import { useSubWalletTheme } from 'hooks/useSubWalletTheme'; import { ThemeTypes } from 'styles/themes'; +import BigN from 'bignumber.js'; +import { BN_ZERO } from 'utils/chainBalances'; interface Props extends TouchableOpacityProps { symbol: string; @@ -19,6 +21,9 @@ interface Props extends TouchableOpacityProps { onSelectNetwork: () => void; defaultItemKey?: string; iconSize?: number; + free?: BigN; + decimals?: number; + price?: number; } export const TokenSelectItem = ({ @@ -31,6 +36,9 @@ export const TokenSelectItem = ({ onSelectNetwork, defaultItemKey, iconSize = 40, + free, + decimals, + price, }: Props) => { const theme = useSubWalletTheme().swThemes; const styles = useMemo(() => createStyle(theme), [theme]); @@ -52,11 +60,24 @@ export const TokenSelectItem = ({ - {isSelected && ( - - + {!!decimals && !new BigN(free || 0).eq(BN_ZERO) && ( + + + )} + + + {isSelected && } + ); @@ -81,6 +102,7 @@ function createStyle(theme: ThemeTypes) { flexDirection: 'row', alignItems: 'center', flex: 1, + paddingRight: theme.paddingXS, }, itemTextStyle: { diff --git a/src/components/common/CancelUnstakeItem/index.tsx b/src/components/common/CancelUnstakeItem/index.tsx index a37f488f5..7cc314dfa 100644 --- a/src/components/common/CancelUnstakeItem/index.tsx +++ b/src/components/common/CancelUnstakeItem/index.tsx @@ -63,7 +63,11 @@ export const CancelUnstakeItem = ({ item, isSelected, onPress }: Props) => { size={theme.fontSize} textStyle={{ ...FontSemiBold }} /> - {isSelected && } + {isSelected ? ( + + ) : ( + + )} ); diff --git a/src/components/common/SelectModal/parts/TokenSelectItem.tsx b/src/components/common/SelectModal/parts/TokenSelectItem.tsx index c0582f624..aedf0ddc1 100644 --- a/src/components/common/SelectModal/parts/TokenSelectItem.tsx +++ b/src/components/common/SelectModal/parts/TokenSelectItem.tsx @@ -13,7 +13,8 @@ interface Props { export function _TokenSelectItem({ item, selectedValueMap, onSelectItem, onCloseModal }: Props) { const chainInfoMap = useSelector((state: RootState) => state.chainStore.chainInfoMap); - const { symbol, originChain, slug, name } = item as TokenItemType; + const { symbol, originChain, slug, name, free, price, decimals } = item as TokenItemType; + return ( ({ item, selectedValueMap, onSelectItem, onCl logoKey={slug.toLowerCase()} subLogoKey={originChain} isSelected={!!selectedValueMap[slug]} + free={free} onSelectNetwork={() => { onSelectItem && onSelectItem(item); onCloseModal && onCloseModal(); }} + decimals={decimals} + price={price} /> ); } diff --git a/src/hooks/screen/Staking/useGetSupportedStakingTokens.ts b/src/hooks/screen/Staking/useGetSupportedStakingTokens.ts index 9bd4e256d..6dc00c948 100644 --- a/src/hooks/screen/Staking/useGetSupportedStakingTokens.ts +++ b/src/hooks/screen/Staking/useGetSupportedStakingTokens.ts @@ -1,4 +1,4 @@ -import { _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; +import { _ChainInfo } from '@subwallet/chain-list/types'; import { StakingType } from '@subwallet/extension-base/background/KoniTypes'; import { AccountJson } from '@subwallet/extension-base/background/types'; import { _STAKING_CHAIN_GROUP } from '@subwallet/extension-base/services/chain-service/constants'; @@ -15,7 +15,8 @@ import { ALL_KEY } from 'constants/index'; import { AccountAddressType } from 'types/index'; import { findAccountByAddress, getAccountAddressType } from 'utils/account'; import useChainAssets from 'hooks/chain/useChainAssets'; -import useChainChecker from 'hooks/chain/useChainChecker'; +import { BN_ZERO } from '@polkadot/util'; +import { TokenItemType } from 'components/Modal/common/TokenSelector'; const isChainTypeValid = (chainInfo: _ChainInfo, accounts: AccountJson[], address?: string): boolean => { const addressType = getAccountAddressType(address); @@ -43,13 +44,15 @@ export default function useGetSupportedStakingTokens( type: StakingType, address?: string, chain?: string, -): _ChainAsset[] { +): TokenItemType[] { const chainInfoMap = useSelector((state: RootState) => state.chainStore.chainInfoMap); const assetRegistryMap = useChainAssets().chainAssetRegistry; - const accounts = useSelector((state: RootState) => state.accountState.accounts); - const { checkChainConnected } = useChainChecker(); + const priceMap = useSelector((state: RootState) => state.price.priceMap); + const { balanceMap } = useSelector((root: RootState) => root.balance); + const { accounts, currentAccount } = useSelector((state: RootState) => state.accountState); return useMemo(() => { - const result: _ChainAsset[] = []; + const result: TokenItemType[] = []; + const accBalanceMap = currentAccount ? balanceMap[currentAccount.address] : undefined; if (type === StakingType.NOMINATED) { Object.values(chainInfoMap).forEach(chainInfo => { @@ -61,7 +64,14 @@ export default function useGetSupportedStakingTokens( isChainTypeValid(chainInfo, accounts, address) && (!chain || chain === ALL_KEY || chain === chainInfo.slug) ) { - result.push(assetRegistryMap[nativeTokenSlug]); + const item = assetRegistryMap[nativeTokenSlug]; + const freeBalance = accBalanceMap[item.slug]?.free || BN_ZERO; + result.push({ + ...item, + price: item.priceId ? priceMap[item.priceId] : 0, + free: accBalanceMap ? freeBalance : BN_ZERO, + decimals: item.decimals || undefined, + }); } } }); @@ -78,26 +88,19 @@ export default function useGetSupportedStakingTokens( isChainTypeValid(chainInfo, accounts, address) && (!chain || chain === ALL_KEY || chain === chainInfo.slug) ) { - result.push(assetRegistryMap[nativeTokenSlug]); + const item = assetRegistryMap[nativeTokenSlug]; + const freeBalance = accBalanceMap[item.slug]?.free || BN_ZERO; + result.push({ + ...item, + price: item.priceId ? priceMap[item.priceId] : 0, + free: accBalanceMap ? freeBalance : BN_ZERO, + decimals: item.decimals || undefined, + }); } } }); } - return result.sort((a, b) => { - if (checkChainConnected(a.originChain)) { - if (checkChainConnected(b.originChain)) { - return 0; - } else { - return -1; - } - } else { - if (checkChainConnected(b.originChain)) { - return 1; - } else { - return 0; - } - } - }); - }, [type, chainInfoMap, assetRegistryMap, accounts, address, chain, checkChainConnected]); + return result; + }, [currentAccount, balanceMap, type, chainInfoMap, assetRegistryMap, accounts, address, chain, priceMap]); } diff --git a/src/screens/Transaction/SendFund/index.tsx b/src/screens/Transaction/SendFund/index.tsx index 377d2b9c0..0e62f20e8 100644 --- a/src/screens/Transaction/SendFund/index.tsx +++ b/src/screens/Transaction/SendFund/index.tsx @@ -6,6 +6,7 @@ import { AssetSetting, ExtrinsicType } from '@subwallet/extension-base/backgroun import { AccountJson } from '@subwallet/extension-base/background/types'; import { _getAssetDecimals, + _getAssetPriceId, _getOriginChainOfAsset, _getTokenMinAmount, _isAssetFungibleToken, @@ -23,14 +24,14 @@ import { SendFundProps } from 'routes/transaction/transactionAction'; import { useSelector } from 'react-redux'; import { RootState } from 'stores/index'; import { getMaxTransfer, makeCrossChainTransfer, makeTransfer, saveRecentAccountId } from 'messaging/index'; -import { findAccountByAddress } from 'utils/account'; +import { findAccountByAddress, getAccountAddressType } from 'utils/account'; import { findNetworkJsonByGenesisHash } from 'utils/getNetworkJsonByGenesisHash'; import { balanceFormatter, formatBalance, formatNumber } from 'utils/number'; import useGetChainPrefixBySlug from 'hooks/chain/useGetChainPrefixBySlug'; import { TokenItemType, TokenSelector } from 'components/Modal/common/TokenSelector'; import useHandleSubmitTransaction, { insufficientMessages } from 'hooks/transaction/useHandleSubmitTransaction'; import { isAccountAll } from 'utils/accountAll'; -import { ChainInfo, ChainItemType } from 'types/index'; +import { AccountAddressType, ChainInfo, ChainItemType } from 'types/index'; import { useSubWalletTheme } from 'hooks/useSubWalletTheme'; import { useToast } from 'react-native-toast-notifications'; import usePreCheckAction from 'hooks/account/usePreCheckAction'; @@ -73,6 +74,7 @@ import { FreeBalanceDisplay } from 'screens/Transaction/parts/FreeBalanceDisplay import { ModalRef } from 'types/modalRef'; import useChainAssets from 'hooks/chain/useChainAssets'; import { TransactionDone } from 'screens/Transaction/TransactionDone'; +import { BalanceItem } from '@subwallet/extension-base/types'; interface TransferFormValues extends TransactionFormValues { to: string; @@ -97,6 +99,8 @@ function getTokenItems( assetRegistry: Record, assetSettingMap: Record, multiChainAssetMap: Record, + balanceMap: Record, + priceMap: Record, tokenGroupSlug?: string, // is ether a token slug or a multiChainAsset slug ): TokenItemType[] { const account = findAccountByAddress(accounts, address); @@ -111,6 +115,7 @@ function getTokenItems( const isAccountEthereum = isEthereumAddress(address); const isSetTokenSlug = !!tokenGroupSlug && !!assetRegistry[tokenGroupSlug]; const isSetMultiChainAssetSlug = !!tokenGroupSlug && !!multiChainAssetMap[tokenGroupSlug]; + const accBalanceMap = balanceMap[address]; if (tokenGroupSlug) { if (!(isSetTokenSlug || isSetMultiChainAssetSlug)) { @@ -143,10 +148,12 @@ function getTokenItems( Object.values(assetRegistry).forEach(chainAsset => { const isValidLedger = isLedger ? isAccountEthereum || validLedgerNetwork.includes(chainAsset?.originChain) : true; const isTokenFungible = _isAssetFungibleToken(chainAsset); - + // @ts-ignore + const freeBalance = accBalanceMap[chainAsset.slug]?.free || BN_ZERO; if (!(isTokenFungible && isAssetTypeValid(chainAsset, chainInfoMap, isAccountEthereum) && isValidLedger)) { return; } + const priceId = _getAssetPriceId(chainAsset); if (isSetMultiChainAssetSlug) { if (chainAsset.multiChainAsset === tokenGroupSlug) { @@ -155,6 +162,9 @@ function getTokenItems( slug: chainAsset.slug, symbol: chainAsset.symbol, originChain: chainAsset.originChain, + free: freeBalance, + price: priceMap[priceId] || 0, + decimals: chainAsset.decimals || undefined, }); } } else { @@ -163,6 +173,9 @@ function getTokenItems( slug: chainAsset.slug, symbol: chainAsset.symbol, originChain: chainAsset.originChain, + free: freeBalance, + price: priceMap[priceId] || 0, + decimals: chainAsset.decimals || undefined, }); } }); @@ -382,6 +395,8 @@ export const SendFund = ({ const { chainInfoMap, chainStateMap } = useSelector((root: RootState) => root.chainStore); const { assetSettingMap, multiChainAssetMap, xcmRefMap } = useSelector((root: RootState) => root.assetRegistry); + const priceMap = useSelector((state: RootState) => state.price.priceMap); + const { balanceMap } = useSelector((root: RootState) => root.balance); const assetRegistry = useChainAssets().chainAssetRegistry; const { accounts, isAllAccount } = useSelector((state: RootState) => state.accountState); const [maxTransfer, setMaxTransfer] = useState('0'); @@ -466,30 +481,19 @@ export const SendFund = ({ assetRegistry, assetSettingMap, multiChainAssetMap, + balanceMap, + priceMap, tokenGroupSlug, - ).sort((a, b) => { - if (checkChainConnected(a.originChain)) { - if (checkChainConnected(b.originChain)) { - return 0; - } else { - return -1; - } - } else { - if (checkChainConnected(b.originChain)) { - return 1; - } else { - return 0; - } - } - }); + ); }, [ accounts, assetRegistry, assetSettingMap, + balanceMap, chainInfoMap, - checkChainConnected, fromValue, multiChainAssetMap, + priceMap, tokenGroupSlug, ]); @@ -963,10 +967,8 @@ export const SendFund = ({ if (tokenItems.length) { let isApplyDefaultAsset = true; - + const account = findAccountByAddress(accounts, from); if (!asset) { - const account = findAccountByAddress(accounts, from); - if (account?.originGenesisHash) { const network = findNetworkJsonByGenesisHash(chainInfoMap, account.originGenesisHash); @@ -985,7 +987,14 @@ export const SendFund = ({ } if (isApplyDefaultAsset) { - updateInfoWithTokenSlug(tokenItems[0].slug); + const a = getAccountAddressType(account?.address); + if (a === AccountAddressType.ETHEREUM) { + const _defaultItem = tokenItems.find(item => item.slug === 'ethereum-NATIVE-ETH'); + updateInfoWithTokenSlug(_defaultItem?.slug || tokenItems[0].slug); + } else { + const _defaultItem = tokenItems.find(item => item.slug === 'polkadot-NATIVE-DOT'); + updateInfoWithTokenSlug(_defaultItem?.slug || tokenItems[0].slug); + } } } }, [accounts, tokenItems, assetRegistry, setChain, chainInfoMap, getValues, setValue]); diff --git a/src/screens/Transaction/Stake/index.tsx b/src/screens/Transaction/Stake/index.tsx index 26f0a8d58..de0230883 100644 --- a/src/screens/Transaction/Stake/index.tsx +++ b/src/screens/Transaction/Stake/index.tsx @@ -520,9 +520,7 @@ export const Stake = ({ selectedValueMap={{ [asset]: true }} onSelectItem={onSelectToken} disabled={stakingChain !== ALL_KEY || !from || loading} - defaultValue={asset} showAddBtn={false} - acceptDefaultValue={true} tokenSelectorRef={tokenSelectorRef} renderSelected={() => } />