diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index 26778ad267..20a131b6b5 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -1720,6 +1720,13 @@ export interface RequestAddPspToken { }; } +// Popular tokens + +export interface TokenPriorityDetails { + tokenGroup: Record; + token: Record +} + /// WalletConnect // Connect @@ -2308,6 +2315,11 @@ export interface KoniRequestSignatures { /* Ledger */ 'pri(ledger.generic.allow)': [null, string[], string[]]; + /* Ledger */ + + /* Popular tokens */ + 'pri(tokens.subscribePriority)': [null, TokenPriorityDetails, TokenPriorityDetails]; + /* Popular tokens */ } export interface ApplicationMetadataType { diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index 273ce331bd..51e5816600 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -8,7 +8,7 @@ import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _MultiChainAsset } from import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; import { withErrorLog } from '@subwallet/extension-base/background/handlers/helpers'; import { createSubscription } from '@subwallet/extension-base/background/handlers/subscriptions'; -import { AccountExternalError, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CronReloadRequest, CrowdloanJson, ExternalRequestPromiseStatus, ExtrinsicType, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, MetadataItem, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangePriceCurrency, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestConfirmationComplete, RequestConfirmationCompleteTon, RequestConnectWalletConnect, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetTransaction, RequestKeyringExportMnemonic, RequestMaxTransferable, RequestMigratePassword, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveAppConfig, RequestSaveBrowserConfig, RequestSaveOSConfig, RequestSaveRecentAccount, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseChangeMasterPassword, ResponseFindRawMetadata, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseNftImport, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSubscribeHistory, ResponseUnlockKeyring, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, StakingType, SufficientMetadata, ThemeNames, TransactionHistoryItem, TransactionResponse, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountExternalError, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CronReloadRequest, CrowdloanJson, ExternalRequestPromiseStatus, ExtrinsicType, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, MetadataItem, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangePriceCurrency, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestConfirmationComplete, RequestConfirmationCompleteTon, RequestConnectWalletConnect, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetTransaction, RequestKeyringExportMnemonic, RequestMaxTransferable, RequestMigratePassword, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveAppConfig, RequestSaveBrowserConfig, RequestSaveOSConfig, RequestSaveRecentAccount, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseChangeMasterPassword, ResponseFindRawMetadata, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseNftImport, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSubscribeHistory, ResponseUnlockKeyring, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, StakingType, SufficientMetadata, ThemeNames, TokenPriorityDetails, TransactionHistoryItem, TransactionResponse, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; import { AccountAuthType, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountExport, RequestAuthorizeCancel, RequestAuthorizeReject, RequestCurrentAccountAddress, RequestMetadataApprove, RequestMetadataReject, RequestSigningApproveSignature, RequestSigningCancel, RequestTypes, ResponseAccountExport, ResponseAuthorizeList, ResponseType, SigningRequest, WindowOpenParams } from '@subwallet/extension-base/background/types'; import { TransactionWarning } from '@subwallet/extension-base/background/warnings/TransactionWarning'; import { ALL_ACCOUNT_KEY, LATEST_SESSION, XCM_FEE_RATIO } from '@subwallet/extension-base/constants'; @@ -3897,6 +3897,24 @@ export default class KoniExtension { /* Ledger */ + /* Popular tokens */ + + private subscribePriorityTokens (id: string, port: chrome.runtime.Port): TokenPriorityDetails { + const cb = createSubscription<'pri(tokens.subscribePriority)'>(id, port); + + const subscription = this.#koniState.chainService.observable.priorityTokens.subscribe(cb); + + this.createUnsubscriptionHandle(id, subscription.unsubscribe); + + port.onDisconnect.addListener((): void => { + this.cancelSubscription(id); + }); + + return this.#koniState.chainService.value.priorityTokens; + } + + /* Popular tokens */ + // -------------------------------------------------------------- // eslint-disable-next-line @typescript-eslint/require-await public async handle (id: string, type: TMessageType, request: RequestTypes[TMessageType], port: chrome.runtime.Port): Promise> { @@ -4507,6 +4525,12 @@ export default class KoniExtension { case 'pri(ledger.generic.allow)': return this.subscribeLedgerGenericAllowChains(id, port); /* Ledger */ + + /* Priority tokens */ + case 'pri(tokens.subscribePriority)': + return this.subscribePriorityTokens(id, port); + /* Priority tokens */ + // Default default: throw new Error(`Unable to handle message of type ${type}`); diff --git a/packages/extension-base/src/services/chain-service/index.ts b/packages/extension-base/src/services/chain-service/index.ts index accbad33e5..4fd95b5048 100644 --- a/packages/extension-base/src/services/chain-service/index.ts +++ b/packages/extension-base/src/services/chain-service/index.ts @@ -3,7 +3,7 @@ import { AssetLogoMap, AssetRefMap, ChainAssetMap, ChainInfoMap, ChainLogoMap, MultiChainAssetMap } from '@subwallet/chain-list'; import { _AssetRef, _AssetRefPath, _AssetType, _ChainAsset, _ChainInfo, _ChainStatus, _EvmInfo, _MultiChainAsset, _SubstrateChainType, _SubstrateInfo, _TonInfo } from '@subwallet/chain-list/types'; -import { AssetSetting, MetadataItem, ValidateNetworkResponse } from '@subwallet/extension-base/background/KoniTypes'; +import { AssetSetting, MetadataItem, TokenPriorityDetails, ValidateNetworkResponse } from '@subwallet/extension-base/background/KoniTypes'; import { _DEFAULT_ACTIVE_CHAINS, _ZK_ASSET_PREFIX, LATEST_CHAIN_DATA_FETCHING_INTERVAL } from '@subwallet/extension-base/services/chain-service/constants'; import { EvmChainHandler } from '@subwallet/extension-base/services/chain-service/handler/EvmChainHandler'; import { MantaPrivateHandler } from '@subwallet/extension-base/services/chain-service/handler/manta/MantaPrivateHandler'; @@ -92,6 +92,7 @@ export class ChainService { private assetLogoMapSubject = new BehaviorSubject>(AssetLogoMap); private chainLogoMapSubject = new BehaviorSubject>(ChainLogoMap); private ledgerGenericAllowChainsSubject = new BehaviorSubject([]); + private priorityTokensSubject = new BehaviorSubject({} as TokenPriorityDetails); // Todo: Update to new store indexed DB private store: AssetSettingStore = new AssetSettingStore(); @@ -123,20 +124,28 @@ export class ChainService { public get value () { const ledgerGenericAllowChains = this.ledgerGenericAllowChainsSubject; + const priorityTokens = this.priorityTokensSubject; return { get ledgerGenericAllowChains () { return ledgerGenericAllowChains.value; + }, + get priorityTokens () { + return priorityTokens.value; } }; } public get observable () { const ledgerGenericAllowChains = this.ledgerGenericAllowChainsSubject; + const priorityTokens = this.priorityTokensSubject; return { get ledgerGenericAllowChains () { return ledgerGenericAllowChains.asObservable(); + }, + get priorityTokens () { + return priorityTokens.asObservable(); } }; } @@ -767,6 +776,11 @@ export class ChainService { this.logger.log('Finished updating latest ledger generic allow chains'); } + handleLatestPriorityTokens (latestPriorityTokens: TokenPriorityDetails) { + this.priorityTokensSubject.next(latestPriorityTokens); + this.logger.log('Finished updating latest popular tokens'); + } + handleLatestData () { this.fetchLatestChainData().then((latestChainInfo) => { this.lockChainInfoMap = true; // do not need to check current lockChainInfoMap because all remains action is fast enough and don't affect this feature. @@ -786,6 +800,12 @@ export class ChainService { this.handleLatestLedgerGenericAllowChains(latestledgerGenericAllowChains); }) .catch(console.error); + + this.fetchLatestPriorityTokens() + .then((latestPriorityTokens) => { + this.handleLatestPriorityTokens(latestPriorityTokens); + }) + .catch(console.error); } private async initApis () { @@ -1088,6 +1108,13 @@ export class ChainService { return await fetchStaticData('chains/ledger-generic-allow-chains') || []; } + private async fetchLatestPriorityTokens () { + return await fetchStaticData('chain-assets/priority-tokens') || { + tokenGroup: {}, + token: {} + }; + } + private async initChains () { const storedChainSettings = await this.dbService.getAllChainStore(); const defaultChainInfoMap = filterChainInfoMap(ChainInfoMap, ignoredList); diff --git a/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailList.tsx b/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailList.tsx index d7a9ef6a1b..8219b65668 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailList.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailList.tsx @@ -18,7 +18,7 @@ import { DetailUpperBlock } from '@subwallet/extension-koni-ui/Popup/Home/Tokens import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { TokenBalanceItemType } from '@subwallet/extension-koni-ui/types/balance'; -import { getTransactionFromAccountProxyValue, isAccountAll, sortTokenByValue } from '@subwallet/extension-koni-ui/utils'; +import { getTransactionFromAccountProxyValue, isAccountAll, sortTokensByStandard } from '@subwallet/extension-koni-ui/utils'; import { isTonAddress } from '@subwallet/keyring'; import { KeypairType } from '@subwallet/keyring/types'; import { ModalContext } from '@subwallet/react-ui'; @@ -72,6 +72,7 @@ function Component (): React.ReactElement { const isAllAccount = useSelector((state: RootState) => state.accountState.isAllAccount); const { tokens } = useSelector((state: RootState) => state.buyService); const swapPairs = useSelector((state) => state.swap.swapPairs); + const priorityTokens = useSelector((root: RootState) => root.chainStore.priorityTokens); const [, setStorage] = useLocalStorage(TRANSFER_TRANSACTION, DEFAULT_TRANSFER_PARAMS); const [, setSwapStorage] = useLocalStorage(SWAP_TRANSACTION, DEFAULT_SWAP_PARAMS); const { banners, dismissBanner, onClickBanner } = useGetBannerByScreen('token_detail', tokenGroupSlug); @@ -190,7 +191,9 @@ function Component (): React.ReactElement { } }); - return items.sort(sortTokenByValue); + sortTokensByStandard(items, priorityTokens); + + return items; } if (tokenBalanceMap[tokenGroupSlug]) { @@ -199,7 +202,7 @@ function Component (): React.ReactElement { } return [] as TokenBalanceItemType[]; - }, [tokenGroupSlug, tokenGroupMap, tokenBalanceMap]); + }, [tokenGroupSlug, tokenGroupMap, tokenBalanceMap, priorityTokens]); const isHaveOnlyTonSoloAcc = useMemo(() => { const checkValidAcc = (currentAcc: AccountProxy) => { diff --git a/packages/extension-koni-ui/src/Popup/Home/Tokens/index.tsx b/packages/extension-koni-ui/src/Popup/Home/Tokens/index.tsx index b60572fc2a..018a2d3f2b 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Tokens/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Tokens/index.tsx @@ -10,14 +10,14 @@ import { TokenGroupBalanceItem } from '@subwallet/extension-koni-ui/components/T import { DEFAULT_SWAP_PARAMS, DEFAULT_TRANSFER_PARAMS, IS_SHOW_TON_CONTRACT_VERSION_WARNING, SWAP_TRANSACTION, TON_ACCOUNT_SELECTOR_MODAL, TON_WALLET_CONTRACT_SELECTOR_MODAL, TRANSFER_TRANSACTION } from '@subwallet/extension-koni-ui/constants'; import { DataContext } from '@subwallet/extension-koni-ui/contexts/DataContext'; import { HomeContext } from '@subwallet/extension-koni-ui/contexts/screen/HomeContext'; -import { useCoreReceiveModalHelper, useGetBannerByScreen, useGetChainSlugsByAccount, useSetCurrentPage } from '@subwallet/extension-koni-ui/hooks'; +import { useCoreReceiveModalHelper, useDebouncedValue, useGetBannerByScreen, useGetChainSlugsByAccount, useSetCurrentPage } from '@subwallet/extension-koni-ui/hooks'; import useNotification from '@subwallet/extension-koni-ui/hooks/common/useNotification'; import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; import { UpperBlock } from '@subwallet/extension-koni-ui/Popup/Home/Tokens/UpperBlock'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, ThemeProps, TransferParams } from '@subwallet/extension-koni-ui/types'; import { TokenBalanceItemType } from '@subwallet/extension-koni-ui/types/balance'; -import { getTransactionFromAccountProxyValue, isAccountAll, sortTokenByValue } from '@subwallet/extension-koni-ui/utils'; +import { getTransactionFromAccountProxyValue, isAccountAll, sortTokensByStandard } from '@subwallet/extension-koni-ui/utils'; import { isTonAddress } from '@subwallet/keyring'; import { Button, Icon, ModalContext, SwAlert } from '@subwallet/react-ui'; import classNames from 'classnames'; @@ -30,6 +30,7 @@ import styled from 'styled-components'; import { useLocalStorage } from 'usehooks-ts'; type Props = ThemeProps; + const tonWalletContractSelectorModalId = TON_WALLET_CONTRACT_SELECTOR_MODAL; const tonAccountSelectorModalId = TON_ACCOUNT_SELECTOR_MODAL; @@ -47,6 +48,7 @@ const Component = (): React.ReactElement => { totalBalanceInfo }, tokenGroupStructure: { sortedTokenGroups } } = useContext(HomeContext); const notify = useNotification(); const { onOpenReceive, receiveModalProps } = useCoreReceiveModalHelper(); + const priorityTokens = useSelector((state: RootState) => state.chainStore.priorityTokens); const isZkModeSyncing = useSelector((state: RootState) => state.mantaPay.isSyncing); const zkModeSyncProgress = useSelector((state: RootState) => state.mantaPay.progress); @@ -291,17 +293,21 @@ const Component = (): React.ReactElement => { navigate('/transaction/swap'); }, [accountProxies, currentAccountProxy, navigate, notify, setSwapStorage, t]); - const tokenGroupBalanceItems = useMemo(() => { + const debouncedTokenGroupBalanceMap = useDebouncedValue>(tokenGroupBalanceMap, 300); + + const tokenGroupBalanceItems = useMemo((): TokenBalanceItemType[] => { const result: TokenBalanceItemType[] = []; sortedTokenGroups.forEach((tokenGroupSlug) => { - if (tokenGroupBalanceMap[tokenGroupSlug]) { - result.push(tokenGroupBalanceMap[tokenGroupSlug]); + if (debouncedTokenGroupBalanceMap[tokenGroupSlug]) { + result.push(debouncedTokenGroupBalanceMap[tokenGroupSlug]); } }); - return result.sort(sortTokenByValue); - }, [sortedTokenGroups, tokenGroupBalanceMap]); + sortTokensByStandard(result, priorityTokens, true); + + return result; + }, [sortedTokenGroups, debouncedTokenGroupBalanceMap, priorityTokens]); useEffect(() => { window.addEventListener('resize', handleResize); diff --git a/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/TokenSelector.tsx b/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/TokenSelector.tsx index 680e886566..0b97ac00a0 100644 --- a/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/TokenSelector.tsx +++ b/packages/extension-koni-ui/src/components/Modal/ReceiveModalNew/parts/TokenSelector.tsx @@ -8,7 +8,9 @@ import TokenEmptyList from '@subwallet/extension-koni-ui/components/EmptyList/To import Search from '@subwallet/extension-koni-ui/components/Search'; import { RECEIVE_MODAL_TOKEN_SELECTOR } from '@subwallet/extension-koni-ui/constants'; import { useSelector, useTranslation } from '@subwallet/extension-koni-ui/hooks'; +import { RootState } from '@subwallet/extension-koni-ui/stores'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { sortTokensByStandard } from '@subwallet/extension-koni-ui/utils'; import { ModalContext, SwList, SwModal } from '@subwallet/react-ui'; import { SwListSectionRef } from '@subwallet/react-ui/es/sw-list'; import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; @@ -31,6 +33,7 @@ function Component ({ className = '', items, onCancel, onSelectItem }: Props): R const [currentSearchText, setCurrentSearchText] = useState(''); // @ts-ignore const chainInfoMap = useSelector((state) => state.chainStore.chainInfoMap); + const priorityTokens = useSelector((state: RootState) => state.chainStore.priorityTokens); const listItems = useMemo(() => { const filteredList = items.filter((item) => { @@ -39,6 +42,12 @@ function Component ({ className = '', items, onCancel, onSelectItem }: Props): R return item.symbol.toLowerCase().includes(currentSearchText.toLowerCase()) || chainName.toLowerCase().includes(currentSearchText.toLowerCase()); }); + if (!currentSearchText) { + sortTokensByStandard(filteredList, priorityTokens); + + return filteredList; + } + if (currentSearchText.toLowerCase() === 'ton') { const tonItemIndex = filteredList.findIndex((item) => item.slug === 'ton-NATIVE-TON'); @@ -50,11 +59,11 @@ function Component ({ className = '', items, onCancel, onSelectItem }: Props): R } } - return filteredList; - } else { return filteredList; } - }, [chainInfoMap, currentSearchText, items]); + + return filteredList; + }, [chainInfoMap, currentSearchText, items, priorityTokens]); const isActive = checkActive(modalId); diff --git a/packages/extension-koni-ui/src/contexts/DataContext.tsx b/packages/extension-koni-ui/src/contexts/DataContext.tsx index 17b25a68a2..7c041b71ba 100644 --- a/packages/extension-koni-ui/src/contexts/DataContext.tsx +++ b/packages/extension-koni-ui/src/contexts/DataContext.tsx @@ -3,7 +3,7 @@ import { ping } from '@subwallet/extension-koni-ui/messaging'; import { persistor, store, StoreName } from '@subwallet/extension-koni-ui/stores'; -import { getMissionPoolData, subscribeAccountsData, subscribeAddressBook, subscribeAssetLogoMaps, subscribeAssetRegistry, subscribeAssetSettings, subscribeAuthorizeRequests, subscribeAuthUrls, subscribeBalance, subscribeBuyServices, subscribeBuyTokens, subscribeCampaignBannerData, subscribeCampaignConfirmationData, subscribeCampaignPopupData, subscribeCampaignPopupVisibility, subscribeChainInfoMap, subscribeChainLogoMaps, subscribeChainStakingMetadata, subscribeChainStateMap, subscribeChainStatusMap, subscribeConfirmationRequests, subscribeConfirmationRequestsTon, subscribeConnectWCRequests, subscribeCrowdloan, subscribeKeyringState, subscribeLedgerGenericAllowNetworks, subscribeMantaPayConfig, subscribeMantaPaySyncingState, subscribeMetadataRequests, subscribeMultiChainAssetMap, subscribeNftCollections, subscribeNftItems, subscribePrice, subscribeProcessingCampaign, subscribeRewardHistory, subscribeSigningRequests, subscribeStaking, subscribeStakingNominatorMetadata, subscribeStakingReward, subscribeSwapPairs, subscribeTransactionRequests, subscribeTxHistory, subscribeUiSettings, subscribeUnreadNotificationCount, subscribeWalletConnectSessions, subscribeWCNotSupportRequests, subscribeXcmRefMap, subscribeYieldMinAmountPercent, subscribeYieldPoolInfo, subscribeYieldPositionInfo, subscribeYieldReward } from '@subwallet/extension-koni-ui/stores/utils'; +import { getMissionPoolData, subscribeAccountsData, subscribeAddressBook, subscribeAssetLogoMaps, subscribeAssetRegistry, subscribeAssetSettings, subscribeAuthorizeRequests, subscribeAuthUrls, subscribeBalance, subscribeBuyServices, subscribeBuyTokens, subscribeCampaignBannerData, subscribeCampaignConfirmationData, subscribeCampaignPopupData, subscribeCampaignPopupVisibility, subscribeChainInfoMap, subscribeChainLogoMaps, subscribeChainStakingMetadata, subscribeChainStateMap, subscribeChainStatusMap, subscribeConfirmationRequests, subscribeConfirmationRequestsTon, subscribeConnectWCRequests, subscribeCrowdloan, subscribeKeyringState, subscribeLedgerGenericAllowNetworks, subscribeMantaPayConfig, subscribeMantaPaySyncingState, subscribeMetadataRequests, subscribeMultiChainAssetMap, subscribeNftCollections, subscribeNftItems, subscribePrice, subscribePriorityTokens, subscribeProcessingCampaign, subscribeRewardHistory, subscribeSigningRequests, subscribeStaking, subscribeStakingNominatorMetadata, subscribeStakingReward, subscribeSwapPairs, subscribeTransactionRequests, subscribeTxHistory, subscribeUiSettings, subscribeUnreadNotificationCount, subscribeWalletConnectSessions, subscribeWCNotSupportRequests, subscribeXcmRefMap, subscribeYieldMinAmountPercent, subscribeYieldPoolInfo, subscribeYieldPositionInfo, subscribeYieldReward } from '@subwallet/extension-koni-ui/stores/utils'; import Bowser from 'bowser'; import React from 'react'; import { Provider } from 'react-redux'; @@ -200,6 +200,7 @@ export const DataContextProvider = ({ children }: DataContextProviderProps) => { _DataContext.addHandler({ ...subscribeChainStatusMap, name: 'subscribeChainStatusMap', relatedStores: ['chainStore'], isStartImmediately: true }); _DataContext.addHandler({ ...subscribeChainInfoMap, name: 'subscribeChainInfoMap', relatedStores: ['chainStore'], isStartImmediately: true }); _DataContext.addHandler({ ...subscribeLedgerGenericAllowNetworks, name: 'subscribeLedgerGenericAllowNetworks', relatedStores: ['chainStore'], isStartImmediately: true }); + _DataContext.addHandler({ ...subscribePriorityTokens, name: 'subscribePriorityTokens', relatedStores: ['chainStore'], isStartImmediately: false }); _DataContext.addHandler({ ...subscribeAssetRegistry, name: 'subscribeAssetRegistry', relatedStores: ['assetRegistry'], isStartImmediately: true }); _DataContext.addHandler({ ...subscribeMultiChainAssetMap, name: 'subscribeMultiChainAssetMap', relatedStores: ['assetRegistry'], isStartImmediately: true }); diff --git a/packages/extension-koni-ui/src/hooks/common/index.ts b/packages/extension-koni-ui/src/hooks/common/index.ts index aa194a3edf..ee1c5536ac 100644 --- a/packages/extension-koni-ui/src/hooks/common/index.ts +++ b/packages/extension-koni-ui/src/hooks/common/index.ts @@ -12,8 +12,8 @@ export { default as useTranslation } from './useTranslation'; export { default as useUILock } from './useUILock'; export { default as useUnlockChecker } from './useUnlockChecker'; export { default as useAlert } from './useAlert'; -export { default as useGetChainSlugsByCurrentAccount } from './useGetChainSlugsByCurrentAccount'; export { default as useSetSessionLatest } from './useSetSessionLatest'; +export { default as useDebouncedValue } from './useDebouncedValue'; export * from './useSelector'; export * from './useLazyList'; diff --git a/packages/extension-koni-ui/src/hooks/common/useDebouncedValue.ts b/packages/extension-koni-ui/src/hooks/common/useDebouncedValue.ts new file mode 100644 index 0000000000..c86b157297 --- /dev/null +++ b/packages/extension-koni-ui/src/hooks/common/useDebouncedValue.ts @@ -0,0 +1,20 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { useEffect, useState } from 'react'; + +function useDebouncedValue (value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => clearTimeout(timer); + }, [value, delay]); + + return debouncedValue; +} + +export default useDebouncedValue; diff --git a/packages/extension-koni-ui/src/stores/feature/common/ChainStore.ts b/packages/extension-koni-ui/src/stores/feature/common/ChainStore.ts index 0ad9c2c927..78c601539d 100644 --- a/packages/extension-koni-ui/src/stores/feature/common/ChainStore.ts +++ b/packages/extension-koni-ui/src/stores/feature/common/ChainStore.ts @@ -3,6 +3,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { _ChainInfo } from '@subwallet/chain-list/types'; +import { TokenPriorityDetails } from '@subwallet/extension-base/background/KoniTypes'; import { _ChainApiStatus, _ChainState } from '@subwallet/extension-base/services/chain-service/types'; import { ChainStore, ReduxStatus } from '@subwallet/extension-koni-ui/stores/types'; @@ -11,6 +12,7 @@ const initialState: ChainStore = { chainStateMap: {}, chainStatusMap: {}, ledgerGenericAllowNetworks: [], + priorityTokens: { tokenGroup: {}, token: {} }, reduxStatus: ReduxStatus.INIT }; @@ -53,6 +55,15 @@ const chainStoreSlice = createSlice({ ledgerGenericAllowNetworks: payload, reduxStatus: ReduxStatus.READY }; + }, + updatePriorityTokens (state, action: PayloadAction) { + const { payload } = action; + + return { + ...state, + priorityTokens: payload, + reduxStatus: ReduxStatus.READY + }; } } }); diff --git a/packages/extension-koni-ui/src/stores/types.ts b/packages/extension-koni-ui/src/stores/types.ts index f339820298..937af918b7 100644 --- a/packages/extension-koni-ui/src/stores/types.ts +++ b/packages/extension-koni-ui/src/stores/types.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { _AssetRef, _ChainAsset, _ChainInfo, _MultiChainAsset } from '@subwallet/chain-list/types'; -import { AddressBookState, AllLogoMap, AssetSetting, CampaignBanner, ChainStakingMetadata, ConfirmationDefinitions, ConfirmationsQueue, ConfirmationsQueueTon, ConfirmationType, CrowdloanItem, KeyringState, LanguageType, MantaPayConfig, NftCollection, NftItem, NominatorMetadata, PriceJson, StakingItem, StakingRewardItem, TransactionHistoryItem, UiSettings, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; +import { AddressBookState, AllLogoMap, AssetSetting, CampaignBanner, ChainStakingMetadata, ConfirmationDefinitions, ConfirmationsQueue, ConfirmationsQueueTon, ConfirmationType, CrowdloanItem, KeyringState, LanguageType, MantaPayConfig, NftCollection, NftItem, NominatorMetadata, PriceJson, StakingItem, StakingRewardItem, TokenPriorityDetails, TransactionHistoryItem, UiSettings, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; import { AccountsContext, AuthorizeRequest, MetadataRequest, SigningRequest } from '@subwallet/extension-base/background/types'; import { _ChainApiStatus, _ChainState } from '@subwallet/extension-base/services/chain-service/types'; import { AppBannerData, AppConfirmationData, AppPopupData } from '@subwallet/extension-base/services/mkt-campaign-service/types'; @@ -126,6 +126,7 @@ export interface ChainStore extends BaseReduxStore { chainStateMap: Record chainStatusMap: Record ledgerGenericAllowNetworks: string[]; + priorityTokens: TokenPriorityDetails; } export interface BalanceStore extends BaseReduxStore { diff --git a/packages/extension-koni-ui/src/stores/utils/index.ts b/packages/extension-koni-ui/src/stores/utils/index.ts index a0d892c268..9fdade6df0 100644 --- a/packages/extension-koni-ui/src/stores/utils/index.ts +++ b/packages/extension-koni-ui/src/stores/utils/index.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { _AssetRef, _ChainAsset, _ChainInfo, _MultiChainAsset } from '@subwallet/chain-list/types'; -import { AddressBookInfo, AssetSetting, CampaignBanner, ChainStakingMetadata, ConfirmationsQueue, ConfirmationsQueueTon, CrowdloanJson, KeyringState, MantaPayConfig, MantaPaySyncState, NftCollection, NftJson, NominatorMetadata, PriceJson, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, TransactionHistoryItem, UiSettings } from '@subwallet/extension-base/background/KoniTypes'; +import { AddressBookInfo, AssetSetting, CampaignBanner, ChainStakingMetadata, ConfirmationsQueue, ConfirmationsQueueTon, CrowdloanJson, KeyringState, MantaPayConfig, MantaPaySyncState, NftCollection, NftJson, NominatorMetadata, PriceJson, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, TokenPriorityDetails, TransactionHistoryItem, UiSettings } from '@subwallet/extension-base/background/KoniTypes'; import { AccountsContext, AuthorizeRequest, ConfirmationRequestBase, MetadataRequest, SigningRequest } from '@subwallet/extension-base/background/types'; import { _ChainApiStatus, _ChainState } from '@subwallet/extension-base/services/chain-service/types'; import { AppBannerData, AppConfirmationData, AppPopupData } from '@subwallet/extension-base/services/mkt-campaign-service/types'; @@ -508,3 +508,11 @@ export const updateUnreadNotificationCountMap = (data: Record) = export const subscribeUnreadNotificationCount = lazySubscribeMessage('pri(inappNotification.subscribeUnreadNotificationCountMap)', null, updateUnreadNotificationCountMap, updateUnreadNotificationCountMap); /* Notification service */ + +/* Priority tokens */ +export const updatePriorityTokens = (data: TokenPriorityDetails) => { + store.dispatch({ type: 'chainStore/updatePriorityTokens', payload: data }); +}; + +export const subscribePriorityTokens = lazySubscribeMessage('pri(tokens.subscribePriority)', null, updatePriorityTokens, updatePriorityTokens); +/* Priority tokens */ diff --git a/packages/extension-koni-ui/src/utils/sort/token.ts b/packages/extension-koni-ui/src/utils/sort/token.ts index 902aab4195..624013a4a3 100644 --- a/packages/extension-koni-ui/src/utils/sort/token.ts +++ b/packages/extension-koni-ui/src/utils/sort/token.ts @@ -1,14 +1,95 @@ // Copyright 2019-2022 @polkadot/extension-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { TokenBalanceItemType } from '@subwallet/extension-koni-ui/types'; +import { TokenPriorityDetails } from '@subwallet/extension-base/background/KoniTypes'; +import { BalanceValueInfo } from '@subwallet/extension-koni-ui/types'; -export const sortTokenByValue = (a: TokenBalanceItemType, b: TokenBalanceItemType): number => { - const convertValue = b.total.convertedValue.minus(a.total.convertedValue).toNumber(); +export interface SortableTokenItem { + slug: string, + symbol: string, + total?: BalanceValueInfo, +} - if (convertValue) { - return convertValue; +export const sortTokenByValue = (a: SortableTokenItem, b: SortableTokenItem): number => { + if (a.total && b.total) { + const convertValue = b.total.convertedValue.minus(a.total.convertedValue).toNumber(); + + if (convertValue) { + return convertValue; + } else { + return b.total.value.minus(a.total.value).toNumber(); + } + } else { + return 0; + } +}; + +export const sortTokenAlphabetically = (a: string, b: string): number => { + const aSymbol = a.toLowerCase(); + const bSymbol = b.toLowerCase(); + + if (aSymbol < bSymbol) { + return -1; + } else if (aSymbol > bSymbol) { + return 1; + } else { + return 0; + } +}; + +export const sortTokenByPriority = (a: string, b: string, aIsPrioritizedToken: boolean, bIsPrioritizedToken: boolean, aPriority: number, bPriority: number): number => { + if (aIsPrioritizedToken && !bIsPrioritizedToken) { + return -1; + } else if (!aIsPrioritizedToken && bIsPrioritizedToken) { + return 1; + } else if (!aIsPrioritizedToken && !bIsPrioritizedToken) { + return sortTokenAlphabetically(a, b); } else { - return b.total.value.minus(a.total.value).toNumber(); + if (aPriority < bPriority) { + return -1; + } else if (aPriority > bPriority) { + return 1; + } else { + return 0; + } } }; + +export function sortTokensByStandard (targetTokens: SortableTokenItem[], priorityTokenGroups: TokenPriorityDetails, isTokenGroup = false) { + const priorityTokenGroupKeys = Object.keys(priorityTokenGroups.tokenGroup); + const priorityTokenKeys = Object.keys(priorityTokenGroups.token); + + targetTokens.sort((a, b) => { + const aHasBalance = (a.total && (a.total.convertedValue.toNumber() !== 0 || a.total.value.toNumber() !== 0)); + const bHasBalance = (b.total && (b.total.convertedValue.toNumber() !== 0 || b.total.value.toNumber() !== 0)); + + if (aHasBalance && bHasBalance) { + return sortTokenByValue(a, b); + } else if (aHasBalance && !bHasBalance) { + return -1; + } else if (!aHasBalance && bHasBalance) { + return 1; + } + + const aSlug = a.slug; + const bSlug = b.slug; + + if (isTokenGroup) { + const aBelongToPrioritizedGroup = priorityTokenGroupKeys.includes(aSlug); + const bBelongToPrioritizedGroup = priorityTokenGroupKeys.includes(bSlug); + + const aPriority = aBelongToPrioritizedGroup ? priorityTokenGroups.tokenGroup[aSlug] : 0; + const bPriority = bBelongToPrioritizedGroup ? priorityTokenGroups.tokenGroup[bSlug] : 0; + + return sortTokenByPriority(a.symbol, b.symbol, aBelongToPrioritizedGroup, bBelongToPrioritizedGroup, aPriority, bPriority); + } else { + const aIsPrioritizedToken = priorityTokenKeys.includes(aSlug); + const bIsPrioritizedToken = priorityTokenKeys.includes(bSlug); + + const aPriority = aIsPrioritizedToken ? priorityTokenGroups.token[aSlug] : 0; + const bPriority = bIsPrioritizedToken ? priorityTokenGroups.token[bSlug] : 0; + + return sortTokenByPriority(a.symbol, b.symbol, aIsPrioritizedToken, bIsPrioritizedToken, aPriority, bPriority); + } + }); +}