chore: refactor token&protocol manage (#3653)

* chore: remove dead code

* chore: useToken params

* chore: useQueryProjects params

* chore: clean dead code

* chore: remove blocked token logic

* chore: remove blocked token logic

* chore: sort

* chore: remove dead code

* chore: remove customize tokens logic

* chore: refactor useTokens

* fix: ignore db error for tokens

* chore: DisplayedToken to parseTokenItem

* chore: patch other DisplayedToken

* fix: token update in token item

* feat: catch db error for protocol data

* fix: sort import

* chore: clean dead code

* fix: bugs
This commit is contained in:
hz002
2026-04-24 20:05:31 +08:00
committed by GitHub
parent 218c840daa
commit 84389e18ff
19 changed files with 441 additions and 1006 deletions

View File

@@ -231,19 +231,14 @@ const TokenAmountInput = ({
tokens: allTokens,
isLoading: isLoadingAllTokens,
isAllTokenLoading, // 包含lpToken
} = useTokens(
currentAccount?.address,
undefined,
selectorOpened.current ? tokenSelectorVisible : true,
} = useTokens(currentAccount?.address, {
visible: selectorOpened.current ? tokenSelectorVisible : true,
updateNonce,
mainnetChainServerId,
undefined,
isFromMode ? lpTokenMode : undefined, // only show lp tokens in from mode
undefined,
!!keyword,
false,
true
);
chainServerId: mainnetChainServerId,
lpTokensOnly: isFromMode ? lpTokenMode : undefined, // only show lp tokens in from mode
searchMode: !!keyword,
realtimeMode: true,
});
const handleSelectToken = useCallback(() => {
if (allTokens.length > 0) {

View File

@@ -198,19 +198,14 @@ const TokenSelect = forwardRef<
tokens: allTokens,
isLoading: isLoadingAllTokens,
isAllTokenLoading, // 包含lp Token的请求
} = useTokens(
useSwapTokenList ? undefined : currentAccount?.address,
undefined,
tokenSelectorVisible,
} = useTokens(useSwapTokenList ? undefined : currentAccount?.address, {
visible: tokenSelectorVisible,
updateNonce,
queryConds.chainServerId,
undefined,
isFromMode ? lpTokenMode : undefined, // only show lp tokens in from mode
undefined,
!!queryConds.keyword,
false,
true
);
chainServerId: queryConds.chainServerId,
lpTokensOnly: isFromMode ? lpTokenMode : undefined, // only show lp tokens in from mode
searchMode: !!queryConds.keyword,
realtimeMode: true,
});
const {
value: swapTokenList,

View File

@@ -701,7 +701,7 @@ function CommonTokenItem(props: {
);
}, [isSwapTo, isBridgeTo, supportChains, chainItem]);
const { value, loading, error } = useAsync(async () => {
const { value: remoteValue, loading, error } = useAsync(async () => {
if (updateToken && currentAccount?.address) {
const data = await wallet.openapi.getToken(
currentAccount?.address,
@@ -710,9 +710,13 @@ function CommonTokenItem(props: {
);
return data;
}
return token;
return undefined;
}, [currentAccount?.address, updateToken, token?.chain, token?.id]);
const value = useMemo(() => {
return remoteValue ? remoteValue : token;
}, [remoteValue, token]);
const tips = useMemo(() => {
return disabled ? t('component.TokenSelector.chainNotSupport') : undefined;
}, [disabled, t]);

View File

@@ -35,11 +35,7 @@ export const formatAppChain = (app: AppChain): DisplayChainWithWhiteLogo => {
isAppChain: true,
};
};
export const useAppChain = (
userAddr: string | undefined,
visible = true,
isTestnet = false
) => {
export const useAppChain = (userAddr: string | undefined, visible = true) => {
const abortProcess = useRef<AbortController>();
const [appChains, setAppChains] = useSafeState<AppChainItem[]>([]);
const [data, setData] = useSafeState<DisplayedProject[]>([]);
@@ -107,11 +103,6 @@ export const useAppChain = (
setData([]);
setHasValue(false);
if (isTestnet) {
setLoading(false);
return;
}
let currentAppChains: AppChainItem[] = [];
const matchedAccount = await wallet.getAccountByAddress(userAddr);
const shouldPersistAppChainCache = matchedAccount

View File

@@ -1,10 +1,7 @@
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import { useWallet } from '../utils/WalletContext';
import { TokenItem } from '@rabby-wallet/rabby-api/dist/types';
import {
DisplayedToken,
encodeProjectTokenId,
} from '../utils/portfolio/project';
import { encodeProjectTokenId } from '../utils/portfolio/project';
import { AbstractPortfolioToken } from '../utils/portfolio/types';
import { useRabbyDispatch, useRabbySelector } from 'ui/store';
import { isSameAddress } from '../utils';
@@ -14,6 +11,7 @@ import { Chain } from '@debank/common';
import useSyncStaleValue from './useDebounceValue';
import { useRefState } from './useRefState';
import { safeBuildRegExp } from '@/utils/string';
import { parseTokenItem } from '../utils/portfolio/tokenUtils';
function isSearchInputWeb3Address(q: string) {
return q.length === 42 && q.toLowerCase().startsWith('0x');
@@ -101,12 +99,10 @@ export function useOperateCustomToken() {
if (!tokenWithAmount) return;
if (tokenWithAmount.is_core) {
return dispatch.account.addBlockedToken(
new DisplayedToken(tokenWithAmount) as AbstractPortfolioToken
);
return dispatch.account.addBlockedToken(parseTokenItem(tokenWithAmount));
} else {
return dispatch.account.addCustomizeToken(
new DisplayedToken(tokenWithAmount) as AbstractPortfolioToken
parseTokenItem(tokenWithAmount)
);
}
}, []);
@@ -116,11 +112,11 @@ export function useOperateCustomToken() {
if (tokenWithAmount?.is_core) {
return dispatch.account.removeBlockedToken(
new DisplayedToken(tokenWithAmount) as AbstractPortfolioToken
parseTokenItem(tokenWithAmount)
);
} else {
return dispatch.account.removeCustomizeToken(
new DisplayedToken(tokenWithAmount) as AbstractPortfolioToken
parseTokenItem(tokenWithAmount)
);
}
}, []);
@@ -224,9 +220,7 @@ export function useFindCustomToken(input?: {
// });
lists.portfolioTokenList = [
...(lists.tokenList.map(
(item) => new DisplayedToken(item)
) as AbstractPortfolioToken[]),
...lists.tokenList.map(parseTokenItem),
// ...matchCustomTokens,
].filter((item) => {
const isBlocked = !!blocked.find((b) =>
@@ -346,9 +340,7 @@ const useSearchToken = (
setIsLoading(false);
setResult(
[
...(list.map(
(item) => new DisplayedToken(item)
) as AbstractPortfolioToken[]),
...list.map((item) => parseTokenItem(item)),
...matchCustomTokens,
].filter((item) => {
const isBlocked = !!blocked.find((b) =>

View File

@@ -1,59 +1,38 @@
import { useCallback, useEffect, useMemo } from 'react';
import dayjs from 'dayjs';
import { useSafeState } from '../safeState';
import { useCallback } from 'react';
import { useTokens } from './token';
import { usePortfolios } from './usePortfolio';
const Cache_Timeout = 5 * 60;
type UseQueryProjectsOptions = {
visible?: boolean;
lpTokenMode?: boolean;
searchMode?: boolean;
autoLoad?: boolean;
};
export const useQueryProjects = (
userAddr: string | undefined,
withHistory = false,
visible: boolean,
isTestnet = false,
lpTokenMode = false,
showBlocked = false,
searchMode = false,
autoLoad = true
{
visible = false,
lpTokenMode = false,
searchMode = false,
autoLoad = true,
}: UseQueryProjectsOptions = {}
) => {
const [time, setTime] = useSafeState(dayjs().subtract(1, 'day'));
const shouldAutoLoad = visible && autoLoad;
useEffect(() => {
if (time!.add(1, 'day').add(Cache_Timeout, 's').isBefore(dayjs())) {
// refreshPositions();
}
}, [time]);
const historyTime = useMemo(() => (withHistory ? time : undefined), [
withHistory,
time,
]);
const {
tokens,
netWorth: tokenNetWorth,
isLoading: isTokensLoading,
isAllTokenLoading,
hasValue: hasTokens,
updateData: updateTokens,
walletProject,
customizeTokens,
blockedTokens,
} = useTokens(
userAddr,
historyTime,
shouldAutoLoad,
0,
undefined,
isTestnet,
lpTokenMode,
showBlocked,
} = useTokens(userAddr, {
visible: shouldAutoLoad,
lpTokensOnly: lpTokenMode,
searchMode,
true // disableRecommended
);
disableRecommended: true,
});
const {
data: portfolios,
@@ -62,32 +41,23 @@ export const useQueryProjects = (
netWorth: portfolioNetWorth,
updateData: updatePortfolio,
removeProtocol,
} = usePortfolios(userAddr, historyTime, shouldAutoLoad, isTestnet);
} = usePortfolios(userAddr, shouldAutoLoad);
const refreshPositions = useCallback(() => {
if (!autoLoad || (!isTokensLoading && !isPortfoliosLoading)) {
updatePortfolio();
updateTokens();
setTime(dayjs().subtract(1, 'day'));
}
}, [
updatePortfolio,
updateTokens,
isTokensLoading,
isPortfoliosLoading,
setTime,
autoLoad,
]);
const grossNetWorth = useMemo(() => tokenNetWorth + portfolioNetWorth!, [
tokenNetWorth,
portfolioNetWorth,
]);
return {
tokenNetWorth,
portfolioNetWorth,
grossNetWorth,
refreshPositions,
refreshTokens: updateTokens,
refreshPortfolios: updatePortfolio,
@@ -97,10 +67,7 @@ export const useQueryProjects = (
hasTokens,
hasPortfolios,
tokens,
customizeTokens,
blockedTokens,
portfolios,
walletProject,
removeProtocol,
};
};

View File

@@ -1,113 +1,83 @@
import { useRef, useEffect, useMemo, useCallback } from 'react';
import produce from 'immer';
import { Dayjs } from 'dayjs';
import { TokenItem } from '@rabby-wallet/rabby-api/dist/types';
import { useRabbyDispatch, useRabbySelector } from 'ui/store';
import { CACHE_VALID_DURATION, TOKEN_SYNC_SCENE } from '@/db/constants';
import {
findChainByEnum,
isTestnet as checkIsTestnet,
findChain,
} from '@/utils/chain';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useAsync } from 'react-use';
import { isFullVersionAccountType } from '@/utils/account';
import { syncDbService } from '@/db/services/syncDbService';
import { useRabbyDispatch, useRabbySelector } from 'ui/store';
import { tokenDbService } from '@/db/services/tokenDbService';
import { useWallet } from '../WalletContext';
import { useSafeState } from '../safeState';
import { log } from './usePortfolio';
import {
PortfolioItem,
PortfolioItemToken,
} from '@rabby-wallet/rabby-api/dist/types';
import { uniqBy } from 'lodash';
import { DisplayedProject, DisplayedToken } from './project';
import { AbstractPortfolioToken } from './types';
import { getMissedTokenPrice } from './utils';
import {
walletProject,
batchQueryTokens,
batchQueryHistoryTokens,
setWalletTokens,
queryTokensCache,
sortWalletTokens,
} from './tokenUtils';
import { TokenItem } from '@rabby-wallet/rabby-api/dist/types';
import { CACHE_VALID_DURATION, TOKEN_SYNC_SCENE } from '@/db/constants';
import { findChain } from '@/utils/chain';
import { isSameAddress } from '..';
import { Token } from 'background/service/preference';
import { log } from './usePortfolio';
import { useSafeState } from '../safeState';
import { useWallet } from '../WalletContext';
import { AbstractPortfolioToken } from './types';
import {
defaultTokenFilter,
includeLpTokensFilter,
isLpToken,
} from './lpToken';
import { useAsync } from 'react-use';
import {
batchQueryTokens,
queryTokensCache,
filterValidChainTokens,
replaceCoreTokens,
replaceTokensWithLatest,
parseTokenItem,
sortTokenItems,
} from './tokenUtils';
let lastResetTokenListAddr = '';
// export const tokenChangeLoadingAtom = atom(false);
const filterDisplayToken = (
tokens: AbstractPortfolioToken[],
blocked: Token[]
) => {
return tokens.filter((token) => {
const chain = findChain({
serverId: token.chain,
});
return (
!blocked.find(
(item) =>
isSameAddress(token._tokenId, item.address) &&
item.chain === token.chain
) && findChainByEnum(chain?.enum)
);
});
};
const buildTokenKey = (token: Pick<TokenItem, 'chain' | 'id'>) =>
`${token.chain}-${token.id.toLowerCase()}`;
const uniqTokens = (tokens: TokenItem[]) => {
return uniqBy(tokens, buildTokenKey);
};
/** 替换核心 token */
const replaceCoreTokens = (tokens: TokenItem[], cacheTokens: TokenItem[]) => {
return uniqTokens([
...tokens.filter((token) => !token.is_core),
...cacheTokens,
]);
type UseTokensOptions = {
visible?: boolean;
updateNonce?: number;
chainServerId?: string;
lpTokensOnly?: boolean;
searchMode?: boolean;
disableRecommended?: boolean;
realtimeMode?: boolean;
};
export const useTokens = (
userAddr: string | undefined,
timeAt?: Dayjs,
visible = true,
updateNonce = 0,
chainServerId?: string,
isTestnet: boolean = chainServerId
? !!findChain({ serverId: chainServerId })?.isTestnet
: false,
lpTokensOnly = false,
showBlocked = false,
searchMode = false,
disableRecommended = false,
realtimeMode = false
{
visible = true,
updateNonce = 0,
chainServerId,
lpTokensOnly = false,
searchMode = false,
disableRecommended = false,
realtimeMode = false,
}: UseTokensOptions = {}
) => {
const abortProcess = useRef<AbortController>();
const [data, setData] = useSafeState(walletProject);
const [isLoading, setLoading] = useSafeState(true);
const [isAllTokenLoading, setIsAllTokenLoading] = useSafeState(true);
const historyTime = useRef<number>();
const historyLoad = useRef<boolean>(false);
const wallet = useWallet();
const dispatch = useRabbyDispatch();
const { mainnetTokens, testnetTokens } = useRabbySelector((store) => ({
mainnetTokens: store.account.tokens,
testnetTokens: store.account.testnetTokens,
}));
const [hasValue, setHasValue] = useSafeState(false);
const [isLoading, setLoading] = useSafeState(true);
const [isAllTokenLoading, setIsAllTokenLoading] = useSafeState(true);
const userAddrRef = useRef('');
const chainIdRef = useRef<string | undefined>(undefined);
// const setTokenChangeLoading = useSetAtom(tokenChangeLoadingAtom);
const abortProcess = useRef<AbortController>();
const callCountRef = useRef(0);
const isTestnet = useMemo(() => {
return chainServerId
? !!findChain({ serverId: chainServerId })?.isTestnet
: false;
}, [chainServerId]);
useEffect(() => {
if (updateNonce === 0) return;
loadProcess();
@@ -133,7 +103,7 @@ export const useTokens = (
}
});
} else {
setData(undefined);
setHasValue(false);
}
return () => {
@@ -144,19 +114,6 @@ export const useTokens = (
};
}, [userAddr, visible, chainServerId]);
useEffect(() => {
if (timeAt) {
historyTime.current = timeAt.unix();
if (!isLoading) {
loadHistory();
}
} else {
historyTime.current = 0;
}
// eslint-disable-next-line
}, [timeAt, isLoading]);
const loadProcess = async ({ forceRefresh = false } = {}) => {
callCountRef.current++;
const callCount = callCountRef.current;
@@ -183,30 +140,26 @@ export const useTokens = (
const currentAbort = new AbortController();
abortProcess.current = currentAbort;
historyLoad.current = false;
setLoading(true);
setIsAllTokenLoading(true);
setHasValue(false);
log('======Start-Tokens======', userAddr);
let _data = produce(walletProject, (draft) => {
draft.netWorth = 0;
draft._netWorth = '$0';
draft._netWorthChange = '-';
draft.netWorthChange = 0;
draft._netWorthChangePercent = '';
});
let _tokens: AbstractPortfolioToken[] = [];
setData(_data);
const blocked = showBlocked
? []
: (await wallet.getBlockedToken()).filter((token) => {
if (isTestnet) {
return checkIsTestnet(token.chain);
} else {
return !checkIsTestnet(token.chain);
}
});
const dispatchTokenList = (tokens: AbstractPortfolioToken[]) => {
const displayTokens = filterValidChainTokens(tokens);
if (isTestnet) {
dispatch.account.setTestnetTokenList(displayTokens);
} else {
dispatch.account.setTokenList(displayTokens);
}
};
const applyTokenItems = (tokens: TokenItem[]) => {
setHasValue(tokens.length > 0);
dispatchTokenList(sortTokenItems(tokens));
};
if (currentAbort.signal.aborted) {
abortedFn();
@@ -215,64 +168,59 @@ export const useTokens = (
let currentAllTokens: TokenItem[] = [];
if (!shouldPersistTokenCache) {
await Promise.all([
tokenDbService.deleteForAddress(userAddr),
syncDbService.deleteSceneForAddress({
address: userAddr,
scene: TOKEN_SYNC_SCENE,
}),
]);
} else {
currentAllTokens = await tokenDbService.queryTokens(userAddr);
/**
* 阶段一:本地 DB 缓存
* 能用缓存就从缓存取,否则删除缓存
*/
try {
if (!shouldPersistTokenCache) {
await Promise.all([
tokenDbService.deleteForAddress(userAddr),
syncDbService.deleteSceneForAddress({
address: userAddr,
scene: TOKEN_SYNC_SCENE,
}),
]);
} else {
currentAllTokens = await tokenDbService.queryTokens(userAddr);
if (currentAbort.signal.aborted) {
abortedFn();
return;
}
if (currentAllTokens.length) {
const chainTokens = currentAllTokens.reduce((m, n) => {
m[n.chain] = m[n.chain] || [];
m[n.chain].push(n);
return m;
}, {} as Record<string, TokenItem[]>);
_data = produce(_data, (draft) => {
setWalletTokens(draft, chainTokens);
});
setData(_data);
_tokens = sortWalletTokens(_data);
if (isTestnet) {
dispatch.account.setTestnetTokenList(
filterDisplayToken(_tokens, blocked)
);
} else {
dispatch.account.setTokenList(filterDisplayToken(_tokens, blocked));
if (currentAbort.signal.aborted) {
abortedFn();
return;
}
if (currentAllTokens.length) {
applyTokenItems(currentAllTokens);
setLoading(false);
}
const updatedAt =
(await syncDbService.getUpdatedAt({
address: userAddr,
scene: TOKEN_SYNC_SCENE,
})) || 0;
const shouldUseDbCache =
currentAllTokens.length > 0 &&
!forceRefresh &&
!realtimeMode &&
updatedAt > Date.now() - CACHE_VALID_DURATION;
if (shouldUseDbCache) {
log('<<==Tokens-cache-hit==>>', userAddr);
setIsAllTokenLoading(false);
return;
}
setLoading(false);
}
const updatedAt =
(await syncDbService.getUpdatedAt({
address: userAddr,
scene: TOKEN_SYNC_SCENE,
})) || 0;
const shouldUseDbCache =
currentAllTokens.length > 0 &&
!forceRefresh &&
!realtimeMode &&
updatedAt > Date.now() - CACHE_VALID_DURATION;
if (shouldUseDbCache) {
log('<<==Tokens-cache-hit==>>', userAddr);
setIsAllTokenLoading(false);
return;
}
} catch (error) {
// 忽略 db 的影响,直接走线上逻辑
log('--Terminate-tokens-db-cache-get', userAddr);
}
/**
* 阶段二:接口快照缓存
* 完成后再决定是否进入完整刷新。
*/
const snapshot = await queryTokensCache(userAddr, wallet, isTestnet);
if (!snapshot) {
@@ -290,28 +238,13 @@ export const useTokens = (
if (snapshot?.length) {
currentAllTokens = replaceCoreTokens(currentAllTokens, snapshot);
const chainTokens = currentAllTokens.reduce((m, n) => {
m[n.chain] = m[n.chain] || [];
m[n.chain].push(n);
return m;
}, {} as Record<string, TokenItem[]>);
_data = produce(_data, (draft) => {
setWalletTokens(draft, chainTokens);
});
setData(_data);
_tokens = sortWalletTokens(_data);
if (isTestnet) {
dispatch.account.setTestnetTokenList(
filterDisplayToken(_tokens, blocked)
);
} else {
dispatch.account.setTokenList(filterDisplayToken(_tokens, blocked));
}
applyTokenItems(currentAllTokens);
setLoading(false);
}
/**
* 阶段三:完整 token 刷新
*/
const tokenRes = await batchQueryTokens(
userAddr,
wallet,
@@ -332,258 +265,43 @@ export const useTokens = (
return;
}
// customize and blocked tokens
const customizeTokens = (await wallet.getCustomizedToken()).filter(
(token) => {
if (isTestnet) {
return checkIsTestnet(token.chain);
} else {
return !checkIsTestnet(token.chain);
}
}
currentAllTokens = replaceTokensWithLatest(
currentAllTokens,
tokenRes,
chainServerId
);
const customTokenList: TokenItem[] = [];
const blockedTokenList: TokenItem[] = [];
tokenRes.forEach((token) => {
if (
customizeTokens.find(
(t) =>
isSameAddress(token.id, t.address) &&
token.chain === t.chain &&
!token.is_core
)
) {
// customize with balance
customTokenList.push(token);
}
if (
blocked.find(
(t) =>
isSameAddress(token.id, t.address) &&
token.chain === t.chain &&
token.is_core
)
) {
blockedTokenList.push(token);
}
});
const apiProvider = isTestnet ? wallet.testnetOpenapi : wallet.openapi;
const noBalanceBlockedTokens = blocked.filter((token) => {
return !blockedTokenList.find(
(t) => isSameAddress(token.address, t.id) && token.chain === t.chain
);
});
const noBalanceCustomizeTokens = customizeTokens.filter((token) => {
return !customTokenList.find(
(t) => isSameAddress(token.address, t.id) && token.chain === t.chain
);
});
if (noBalanceCustomizeTokens.length > 0) {
const noBalanceCustomTokens = await apiProvider.customListToken(
noBalanceCustomizeTokens.map((item) => `${item.chain}:${item.address}`),
userAddr
);
customTokenList.push(
...noBalanceCustomTokens.filter((token) => !token.is_core)
);
}
if (noBalanceBlockedTokens.length > 0) {
const blockedTokens = await apiProvider.customListToken(
noBalanceBlockedTokens.map((item) => `${item.chain}:${item.address}`),
userAddr
);
blockedTokenList.push(...blockedTokens.filter((token) => token.is_core));
}
if (currentAbort.signal.aborted) {
abortedFn();
return;
}
const formattedCustomTokenList = customTokenList.map(
(token) => new DisplayedToken(token) as AbstractPortfolioToken
);
const formattedBlockedTokenList = blockedTokenList.map(
(token) => new DisplayedToken(token) as AbstractPortfolioToken
);
if (isTestnet) {
dispatch.account.setTestnetBlockedTokenList(formattedBlockedTokenList);
dispatch.account.setTestnetCustomizeTokenList(formattedCustomTokenList);
} else {
dispatch.account.setBlockedTokenList(formattedBlockedTokenList);
dispatch.account.setCustomizeTokenList(formattedCustomTokenList);
}
currentAllTokens = uniqTokens([
...(chainServerId
? currentAllTokens.filter((token) => token.chain !== chainServerId)
: []),
...tokenRes,
...customTokenList,
...blockedTokenList,
]);
if (currentAbort.signal.aborted) {
abortedFn();
return;
}
if (shouldPersistTokenCache) {
await tokenDbService.replaceAddressTokens(userAddr, currentAllTokens);
try {
await tokenDbService.replaceAddressTokens(userAddr, currentAllTokens);
if (!chainServerId) {
await syncDbService.setUpdatedAt({
address: userAddr,
scene: TOKEN_SYNC_SCENE,
updatedAt: Date.now(),
});
if (!chainServerId) {
await syncDbService.setUpdatedAt({
address: userAddr,
scene: TOKEN_SYNC_SCENE,
updatedAt: Date.now(),
});
}
} catch (error) {
// 忽略 db 的影响,不写缓存,直走内存
log('--Terminate-tokens-db-cache-set', userAddr);
}
}
const tokensDict: Record<string, TokenItem[]> = {};
tokenRes.forEach((token) => {
if (!tokensDict[token.chain]) {
tokensDict[token.chain] = [];
}
tokensDict[token.chain].push(token);
});
_data = produce(_data, (draft) => {
setWalletTokens(draft, tokensDict);
});
setData(_data);
_tokens = sortWalletTokens(_data);
if (isTestnet) {
dispatch.account.setTestnetTokenList([
...filterDisplayToken(_tokens, blocked),
...formattedCustomTokenList,
]);
} else {
dispatch.account.setTokenList([
...filterDisplayToken(_tokens, blocked),
...formattedCustomTokenList,
]);
if (currentAbort.signal.aborted) {
log('--Terminate-tokens-db-cache-set', userAddr);
abortedFn();
return;
}
applyTokenItems(currentAllTokens);
setLoading(false);
setIsAllTokenLoading(false);
loadHistory(_data, currentAbort);
log('<<==Tokens-end==>>', userAddr);
};
const loadHistory = async (
pre?: DisplayedProject,
currentAbort = new AbortController()
) => {
if (!historyTime.current || !userAddr || historyLoad.current || isTestnet) {
log('middle-tokens-end');
return;
}
abortProcess.current = currentAbort;
historyLoad.current = true;
let _data = pre || data!;
log('===token===batchhistory====', userAddr);
// setTokenChangeLoading(true);
if (currentAbort.signal.aborted || !_data?.netWorth) {
return;
}
const historyTokenRes = await batchQueryHistoryTokens(
userAddr,
historyTime.current,
wallet,
isTestnet
);
if (currentAbort.signal.aborted) {
return;
}
const historyPortfolios: PortfolioItem[] = [];
historyTokenRes?.forEach((token) => {
const chain = token.chain;
const index = historyPortfolios.findIndex((p) => p.pool.id === chain);
if (index === -1) {
historyPortfolios.push({
pool: {
id: chain,
},
asset_token_list: [token as PortfolioItemToken],
} as PortfolioItem);
} else {
historyPortfolios[index].asset_token_list?.push(
token as PortfolioItemToken
);
}
});
_data = produce(_data, (draft) => {
draft.patchHistory(historyPortfolios);
});
const tokenList = sortWalletTokens(_data);
if (isTestnet) {
dispatch.account.setTestnetTokenList(tokenList);
} else {
dispatch.account.setTokenList(tokenList);
}
setData(_data);
if (currentAbort.signal.aborted) {
return;
}
const missedTokens = tokenList.reduce((m, n) => {
if (n._tokenId && !n._historyPatched) {
m[n.chain] = m[n.chain] || new Set();
m[n.chain].add(n._tokenId);
}
return m;
}, {} as Record<string, Set<string>>);
const priceDicts = await getMissedTokenPrice(
missedTokens,
historyTime.current,
wallet
);
if (currentAbort.signal.aborted || !priceDicts) {
return;
}
_data = produce(_data, (draft) => {
Object.entries(priceDicts).forEach(([c, dict]) => {
if (!draft._portfolioDict[c]._historyPatched) {
draft._portfolioDict[c].patchPrice(dict);
if (draft._portfolioDict[c].netWorthChange) {
draft.netWorthChange += draft._portfolioDict[c].netWorthChange;
}
}
draft.afterHistoryPatched();
});
}) as DisplayedProject;
if (currentAbort.signal.aborted) {
return;
}
setData(_data);
if (isTestnet) {
dispatch.account.setTestnetTokenList(sortWalletTokens(_data));
} else {
dispatch.account.setTokenList(sortWalletTokens(_data));
}
};
useEffect(() => {
return () => {
abortProcess.current?.abort();
@@ -630,9 +348,7 @@ export const useTokens = (
chainServerId || ''
);
return list.map(
(token) => new DisplayedToken(token) as AbstractPortfolioToken
);
return list.map(parseTokenItem);
}, [shouldLoadRecommended, userAddr, chainServerId]);
const tokens = useMemo(() => {
@@ -660,16 +376,10 @@ export const useTokens = (
}, [loadProcess]);
return {
netWorth: data?.netWorth || 0,
isLoading: isLoading || loadingRecommendedTokens,
isAllTokenLoading: isAllTokenLoading || loadingRecommendedTokens,
tokens,
customizeTokens: isTestnet
? testnetTokens.customize
: mainnetTokens.customize,
blockedTokens: isTestnet ? testnetTokens.blocked : mainnetTokens.blocked,
hasValue: !!data?._portfolios?.length,
hasValue,
updateData: forceRefresh,
walletProject: data,
};
};

View File

@@ -5,13 +5,19 @@ import {
TokenItemWithEntity,
} from '@rabby-wallet/rabby-api/dist/types';
import { CHAINS } from 'consts';
import { DisplayedProject } from './project';
import { DisplayedProject, encodeProjectTokenId } from './project';
import { WalletControllerType } from '../WalletContext';
import { requestOpenApiWithChainId } from '@/ui/utils/openapi';
import { isTestnet as checkIsTestnet } from '@/utils/chain';
import {
isTestnet as checkIsTestnet,
findChain,
findChainByEnum,
} from '@/utils/chain';
import { pQueue } from './utils';
import { flatten } from 'lodash';
import { isSameAddress } from '..';
import { flatten, uniqBy } from 'lodash';
import { formatAmount, formatPrice, formatUsdValue, isSameAddress } from '..';
import { AbstractPortfolioToken } from './types';
import { getTokenSymbol } from '../token';
export const queryTokensCache = async (
user_id: string,
@@ -176,3 +182,102 @@ export const scamTokenFilter = (item: {
}
return true;
};
export const buildTokenKey = (token: Pick<TokenItem, 'chain' | 'id'>) =>
`${token.chain}-${token.id.toLowerCase()}`;
export const uniqTokens = (tokens: TokenItem[]) => {
return uniqBy(tokens, buildTokenKey);
};
// 过滤掉无效的链
export const filterValidChainTokens = (tokens: AbstractPortfolioToken[]) => {
return tokens.filter((token) => {
const chain = findChain({
serverId: token.chain,
});
return findChainByEnum(chain?.enum);
});
};
/** 替换核心 token (缓存接口没有非 core 的 token */
export const replaceCoreTokens = (
tokens: TokenItem[],
cacheTokens: TokenItem[]
) => {
return uniqTokens([
...tokens.filter((token) => !token.is_core),
...cacheTokens,
]);
};
export const replaceTokensWithLatest = (
tokens: TokenItem[],
latestTokens: TokenItem[],
chainServerId?: string
) => {
if (!chainServerId) {
return uniqTokens(latestTokens);
}
return uniqTokens([
...latestTokens,
...tokens.filter((token) => token.chain !== chainServerId),
]);
};
export const groupTokensByChain = (tokens: TokenItem[]) => {
return tokens.reduce((m, n) => {
m[n.chain] = m[n.chain] || [];
m[n.chain].push(n);
return m;
}, {} as Record<string, TokenItem[]>);
};
export const parseTokenItem = (token: TokenItem): AbstractPortfolioToken => {
const formattedPrice = token.price || 0;
const formattedAmount = token.amount || 0;
const realUsdValue = formattedPrice * formattedAmount;
const usdValue = Math.abs(realUsdValue);
return {
id: encodeProjectTokenId(token),
_tokenId: token.id,
chain: token.chain,
symbol: getTokenSymbol(token),
logo_url: token.logo_url,
amount: formattedAmount,
price: formattedPrice,
_realUsdValue: realUsdValue,
// 注意这里debt 也被处理成正值
_usdValue: usdValue,
_amountStr: formatAmount(Math.abs(formattedAmount)),
_priceStr: formatPrice(formattedPrice),
_usdValueStr: formatUsdValue(usdValue),
decimals: token.decimals,
display_symbol: token.display_symbol,
name: token.name,
optimized_symbol: token.optimized_symbol,
is_core: token.is_core,
is_wallet: token.is_wallet,
is_verified: token.is_verified,
is_suspicious: token.is_suspicious,
time_at: token.time_at,
price_24h_change: token.price_24h_change,
low_credit_score: token.low_credit_score,
raw_amount_hex_str: token.raw_amount_hex_str,
cex_ids: token.cex_ids || [],
protocol_id: token.protocol_id,
_amountChangeStr: '',
_usdValueChangeStr: '-',
_amountChangeUsdValueStr: '',
};
};
export const sortTokenItems = (tokens: TokenItem[]) => {
return tokens
.map(parseTokenItem)
.sort((m, n) => (n._usdValue || 0) - (m._usdValue || 0));
};

View File

@@ -1,29 +1,25 @@
import { useCallback, useEffect, useRef } from 'react';
import produce from 'immer';
import { Dayjs } from 'dayjs';
// import { atom, useSetAtom } from 'jotai';
import { CACHE_VALID_DURATION, DEFI_SYNC_SCENE } from '@/db/constants';
import produce from 'immer';
import { ComplexProtocol } from '@rabby-wallet/rabby-api/dist/types';
import { isFullVersionAccountType } from '@/utils/account';
import { defiDbService } from '@/db/services/defiDbService';
import { syncDbService } from '@/db/services/syncDbService';
import { isFullVersionAccountType } from '@/utils/account';
import { CHAIN_ID_LIST, syncChainIdList } from 'consts';
import { useWallet } from '../WalletContext';
import { chunk, loadTestnetPortfolioSnapshot } from './utils';
import { CACHE_VALID_DURATION, DEFI_SYNC_SCENE } from '@/db/constants';
import { chunk } from './utils';
import { isSameAddress } from '..';
import { useSafeState } from '../safeState';
import { useWallet } from '../WalletContext';
import { DisplayedProject } from './project';
import { getExpandListSwitch } from './expandList';
import {
batchLoadProjects,
batchLoadHistoryProjects,
loadPortfolioSnapshot,
snapshot2Display,
portfolio2Display,
patchPortfolioHistory,
getMissedTokenPrice,
snapshot2Display,
} from './utils';
import { DisplayedProject } from './project';
import { isSameAddress } from '..';
import { ComplexProtocol } from '@rabby-wallet/rabby-api/dist/types';
const chunkSize = 5;
@@ -47,26 +43,16 @@ export const log = (...args: any) => {
// console.log(...args);
};
// export const portfolioChangeLoadingAtom = atom(true);
export const usePortfolios = (
userAddr: string | undefined,
timeAt?: Dayjs,
visible = true,
isTestnet = false
) => {
export const usePortfolios = (userAddr: string | undefined, visible = true) => {
const [data, setData] = useSafeState<DisplayedProject[]>([]);
const [netWorth, setNetWorth] = useSafeState(0);
const [hasValue, setHasValue] = useSafeState(false);
const abortProcess = useRef<AbortController>();
const [isLoading, setLoading] = useSafeState(true);
const projectDict = useRef<Record<string, DisplayedProject> | null>({});
const historyTime = useRef<number>();
const historyLoad = useRef<boolean>(false);
const realtimeIds = useRef<string[]>([]);
const wallet = useWallet();
const userAddrRef = useRef('');
// const setPortfolioChangeLoading = useSetAtom(portfolioChangeLoadingAtom);
useEffect(() => {
let timer: ReturnType<typeof setTimeout> | null = null;
@@ -93,19 +79,6 @@ export const usePortfolios = (
};
}, [userAddr, visible]);
useEffect(() => {
if (timeAt) {
historyTime.current = timeAt.unix();
if (!isLoading) {
loadHistory();
}
} else {
historyTime.current = 0;
}
// eslint-disable-next-line
}, [timeAt, isLoading]);
const applyProtocols = (protocols: ComplexProtocol[]) => {
const _hasValue = protocols.some((x) => Object.keys(x).length > 0);
setHasValue(_hasValue);
@@ -137,69 +110,70 @@ export const usePortfolios = (
const currentAbort = new AbortController();
abortProcess.current = currentAbort;
historyLoad.current = false;
setLoading(true);
// setPortfolioChangeLoading(withHistory);
log('======Start-Portfolio======', userAddr);
setData([]);
setHasValue(false);
let currentProtocols: ComplexProtocol[] = [];
const matchedAccount = isTestnet
? null
: await wallet.getAccountByAddress(userAddr);
const shouldPersistDefiCache =
!isTestnet && matchedAccount
? isFullVersionAccountType(matchedAccount as any)
: false;
const matchedAccount = await wallet.getAccountByAddress(userAddr);
const shouldPersistDefiCache = matchedAccount
? isFullVersionAccountType(matchedAccount as any)
: false;
if (shouldPersistDefiCache) {
currentProtocols = await defiDbService.queryProtocols(userAddr);
/**
* 阶段一:本地 DB 缓存
*/
try {
if (shouldPersistDefiCache) {
currentProtocols = await defiDbService.queryProtocols(userAddr);
if (currentAbort.signal.aborted) {
log('--Terminate-portfolio-db-cache-', userAddr);
setLoading(false);
return;
if (currentAbort.signal.aborted) {
log('--Terminate-portfolio-db-cache-', userAddr);
setLoading(false);
return;
}
if (currentProtocols.length) {
applyProtocols(currentProtocols);
setLoading(false);
}
const updatedAt =
(await syncDbService.getUpdatedAt({
address: userAddr,
scene: DEFI_SYNC_SCENE,
})) || 0;
const shouldUseDbCache =
currentProtocols.length > 0 &&
!forceRefresh &&
updatedAt > Date.now() - CACHE_VALID_DURATION;
if (shouldUseDbCache) {
log('<<==Defi-cache-hit==>>', userAddr);
return;
}
} else {
await Promise.all([
defiDbService.deleteForAddress(userAddr),
syncDbService.deleteSceneForAddress({
address: userAddr,
scene: DEFI_SYNC_SCENE,
}),
]);
}
if (currentProtocols.length) {
applyProtocols(currentProtocols);
setLoading(false);
}
const updatedAt =
(await syncDbService.getUpdatedAt({
address: userAddr,
scene: DEFI_SYNC_SCENE,
})) || 0;
const shouldUseDbCache =
currentProtocols.length > 0 &&
!forceRefresh &&
updatedAt > Date.now() - CACHE_VALID_DURATION;
if (shouldUseDbCache) {
log('<<==Defi-cache-hit==>>', userAddr);
return;
}
} else if (!isTestnet) {
await Promise.all([
defiDbService.deleteForAddress(userAddr),
syncDbService.deleteSceneForAddress({
address: userAddr,
scene: DEFI_SYNC_SCENE,
}),
]);
} catch (error) {
// 忽略 db 的影响,直接走线上逻辑
log('--Terminate-portfolio-db-cache-get', userAddr);
}
/**
* 阶段二:接口快照缓存
*/
let snapshotRes: ComplexProtocol[] = [];
if (isTestnet) {
snapshotRes = await loadTestnetPortfolioSnapshot(userAddr, wallet);
} else {
snapshotRes = await loadPortfolioSnapshot(userAddr, wallet);
}
snapshotRes = await loadPortfolioSnapshot(userAddr, wallet);
if (currentAbort.signal.aborted || !snapshotRes) {
log('--Terminate-portfolio-snapshot-', userAddr);
@@ -220,12 +194,20 @@ export const usePortfolios = (
if (!realtimeIds.current.length) {
if (shouldPersistDefiCache) {
await defiDbService.replaceAddressProtocols(userAddr, currentProtocols);
await syncDbService.setUpdatedAt({
address: userAddr,
scene: DEFI_SYNC_SCENE,
updatedAt: Date.now(),
});
try {
await defiDbService.replaceAddressProtocols(
userAddr,
currentProtocols
);
await syncDbService.setUpdatedAt({
address: userAddr,
scene: DEFI_SYNC_SCENE,
updatedAt: Date.now(),
});
} catch (error) {
// 忽略 db 的影响,不写缓存,直走内存
log('--Terminate-portfolio-db-cache-set', userAddr);
}
}
log('--Terminate-portfolio-loadProjectIds-', userAddr);
@@ -234,6 +216,10 @@ export const usePortfolios = (
return;
}
/**
* 阶段三:完整逐个 id刷新
*/
const chunkIds = chunk(realtimeIds.current, chunkSize);
let realtimeData: DisplayedProject[] = [];
@@ -249,7 +235,7 @@ export const usePortfolios = (
userAddr,
ids,
wallet,
isTestnet
false
);
const projects = projectListRes;
@@ -271,15 +257,34 @@ export const usePortfolios = (
})
);
if (currentAbort.signal.aborted) {
log('--Terminate-portfolio-realtime-', userAddr);
projectDict.current = null;
setLoading(false);
return;
}
currentProtocols = replaceProtocols(currentProtocols, realtimeProtocols);
if (shouldPersistDefiCache) {
await defiDbService.replaceAddressProtocols(userAddr, currentProtocols);
await syncDbService.setUpdatedAt({
address: userAddr,
scene: DEFI_SYNC_SCENE,
updatedAt: Date.now(),
});
try {
await defiDbService.replaceAddressProtocols(userAddr, currentProtocols);
await syncDbService.setUpdatedAt({
address: userAddr,
scene: DEFI_SYNC_SCENE,
updatedAt: Date.now(),
});
} catch (error) {
// 忽略 db 的影响,不写缓存,直走内存
log('--Terminate-portfolio-db-cache-set', userAddr);
}
}
if (currentAbort.signal.aborted) {
log('--Terminate-portfolio-db-cache-set', userAddr);
projectDict.current = null;
setLoading(false);
return;
}
realtimeData = Object.values(projectDict.current)?.sort(
@@ -290,126 +295,9 @@ export const usePortfolios = (
setNetWorth(realtimeData.reduce((m, n) => m + n.netWorth, 0));
setLoading(false);
loadHistory(currentAbort);
log('portfolios-end', userAddr);
};
const loadHistory = async (currentAbort = new AbortController()) => {
if (
!historyTime.current ||
currentAbort.signal.aborted ||
!userAddr ||
historyLoad.current
) {
return;
}
historyLoad.current = true;
syncChainIdList();
const historyIds = realtimeIds.current.filter(
(x) =>
projectDict.current![x].chain &&
CHAIN_ID_LIST.get(projectDict.current![x].chain!)?.isSupportHistory
);
const historyIdsArr = chunk(historyIds, chunkSize);
if (currentAbort.signal.aborted || !historyIdsArr.length) {
return;
}
await Promise.all(
historyIdsArr.map(async (ids) => {
const historyProjectListRes = await batchLoadHistoryProjects(
userAddr,
ids,
wallet,
historyTime.current,
isTestnet
);
const projects = historyProjectListRes;
if (!projects?.length) {
return;
}
projects.forEach((project) => {
if (!currentAbort.signal.aborted && projectDict.current) {
projectDict.current = produce(projectDict.current, (draft) => {
patchPortfolioHistory(project, draft);
});
}
});
})
);
if (currentAbort.signal.aborted) {
return;
}
const historyList = Object.values(projectDict.current!)?.sort(
(m, n) => (n.netWorth || 0) - (m.netWorth || 0)
);
setData(historyList);
// 可能有获取失败的,也需要通过 priceChange 来算大概的变化
const notSuportHistoryProjects = realtimeIds.current.filter(
(x) => !projectDict.current![x]._historyPatched
);
// 是否存在没有被 patchHistory 的(不支持历史结点 | 获取失败的),需要再去请求它之前的价格来计算大致的 usdChange
const missedTokens = notSuportHistoryProjects.reduce((m, n) => {
const pChain = projectDict.current![n]?.chain;
if (!pChain) {
return m;
}
m[pChain] = m[pChain] || new Set();
projectDict.current![n]._portfolios?.forEach((x) => {
x._tokenList?.forEach((t) => {
m[pChain].add(t._tokenId);
});
});
return m;
}, {} as Record<string, Set<string>>);
if (currentAbort.signal.aborted) {
return;
}
const priceDicts = await getMissedTokenPrice(
missedTokens,
historyTime.current,
wallet,
isTestnet
);
if (currentAbort.signal.aborted || !projectDict.current || !priceDicts) {
return;
}
projectDict.current = produce(projectDict.current, (draft) => {
notSuportHistoryProjects?.forEach((pId) => {
if (priceDicts?.[draft[pId].chain!]) {
draft[pId].patchPrice(priceDicts?.[draft[pId].chain!]);
}
});
});
if (currentAbort.signal.aborted) {
return;
}
const priceProjects = Object.values(projectDict.current!)?.sort(
(m, n) => (n.netWorth || 0) - (m.netWorth || 0)
);
setData(priceProjects);
// setPortfolioChangeLoading(false);
};
const removeProtocol = useCallback(
(id: string) => {
setData((pre) => pre?.filter((item) => item.id !== id));

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useMemo } from 'react';
import { TokenSearchInput } from './TokenSearchInput';
import AddTokenEntry, { AddTokenEntryInst } from './AddTokenEntry';
import { useRabbySelector } from '@/ui/store';
import { HomeTokenList } from './TokenList';
import useSortTokens from 'ui/hooks/useSortTokens';
@@ -17,8 +16,6 @@ import { useAppChain } from '@/ui/hooks/useAppChain';
import { useCommonPopupView } from '@/ui/utils';
import { useTranslation } from 'react-i18next';
import { LpTokenSwitch } from '../../DesktopProfile/components/TokensTabPane/components/LpTokenSwitch';
import clsx from 'clsx';
import { ReactComponent as SearchSVG } from '@/ui/assets/search.svg';
import { HomePerpsPositionList } from './HomePerpsPositionList';
import { uniqBy } from 'lodash';
import { concatAndSort } from '@/ui/utils/portfolio/tokenUtils';
@@ -28,7 +25,6 @@ interface Props {
selectChainId: string | null;
visible: boolean;
onEmptyAssets: (isEmpty: boolean) => void;
isTestnet?: boolean;
}
export const AssetListContainer: React.FC<Props> = ({
@@ -36,7 +32,6 @@ export const AssetListContainer: React.FC<Props> = ({
selectChainId,
visible,
onEmptyAssets,
isTestnet = false,
}) => {
const { t } = useTranslation();
const [search, setSearch] = React.useState<string>('');
@@ -56,22 +51,16 @@ export const AssetListContainer: React.FC<Props> = ({
portfolios,
tokens: tokenList,
hasTokens,
blockedTokens,
customizeTokens,
removeProtocol,
} = useQueryProjects(
currentAccount?.address,
false,
} = useQueryProjects(currentAccount?.address, {
visible,
isTestnet,
lpTokenMode ? lpTokenMode : undefined,
undefined,
!!search
);
lpTokenMode: lpTokenMode ? lpTokenMode : undefined,
searchMode: !!search,
});
const {
data: appPortfolios,
isLoading: isAppPortfoliosLoading,
} = useAppChain(currentAccount?.address, visible, isTestnet);
} = useAppChain(currentAccount?.address, visible);
const inputRef = React.useRef<InputRef>(null);
const { isLoading: isSearching, list } = useSearchToken(
@@ -80,7 +69,7 @@ export const AssetListContainer: React.FC<Props> = ({
{
chainServerId: selectChainId ? selectChainId : undefined,
withBalance: true,
isTestnet: isTestnet,
isTestnet: false,
}
);
const displayTokenList = useMemo(() => {
@@ -107,27 +96,11 @@ export const AssetListContainer: React.FC<Props> = ({
return combinedPortfolios;
}, [portfolios, appPortfolios, selectChainId]);
const displayBlockedTokens = useMemo(() => {
if (selectChainId) {
return blockedTokens?.filter((item) => item.chain === selectChainId);
}
return blockedTokens;
}, [blockedTokens, selectChainId]);
const displayCustomizeTokens = useMemo(() => {
if (selectChainId) {
return customizeTokens?.filter((item) => item.chain === selectChainId);
}
return customizeTokens;
}, [customizeTokens, selectChainId]);
const isEmptyAssets =
!isTokensLoading &&
!displayTokenList.length &&
!isPortfoliosLoading &&
!displayPortfolios?.length &&
!displayBlockedTokens?.length &&
!displayCustomizeTokens?.length &&
!isAppPortfoliosLoading &&
!appPortfolios?.length;
@@ -141,10 +114,6 @@ export const AssetListContainer: React.FC<Props> = ({
kw: search,
});
const handleFocusInput = React.useCallback(() => {
inputRef.current?.focus();
}, []);
React.useEffect(() => {
if (!visible) {
setSearch('');
@@ -170,8 +139,6 @@ export const AssetListContainer: React.FC<Props> = ({
return [...new Set(appPortfolios?.map((item) => item.id) || [])];
}, [appPortfolios]);
const addTokenEntryRef = React.useRef<AddTokenEntryInst>(null);
if (isTokensLoading && !hasTokens) {
return <TokenListViewSkeleton />;
}
@@ -202,7 +169,6 @@ export const AssetListContainer: React.FC<Props> = ({
onLpTokenModeChange={setLpTokenMode}
/>
</div>
{/* {isFocus || search ? null : <AddTokenEntry ref={addTokenEntryRef} />} */}
</div>
{isTokensLoading || isSearching || (lpTokenMode && isAllTokenLoading) ? (
<TokenListSkeleton />
@@ -210,17 +176,9 @@ export const AssetListContainer: React.FC<Props> = ({
<div className="mt-[12px]">
<HomeTokenList
list={sortTokens}
onFocusInput={handleFocusInput}
onOpenAddEntryPopup={() => {
addTokenEntryRef.current?.startAddToken();
}}
isSearch={!!search}
lpTokenMode={lpTokenMode}
isNoResults={isNoResults}
blockedTokens={displayBlockedTokens}
customizeTokens={displayCustomizeTokens}
isTestnet={isTestnet}
selectChainId={selectChainId}
/>
</div>
)}

View File

@@ -4,36 +4,17 @@ import { useExpandList } from '@/ui/utils/portfolio/expandList';
import BigNumber from 'bignumber.js';
import { TokenLowValueItem } from './TokenLowValueItem';
import { TokenTable } from './components/TokenTable';
import { BlockedButton } from './BlockedButton';
import { CustomizedButton } from './CustomizedButton';
import { TokenListEmpty } from './TokenListEmpty';
import { useTranslation } from 'react-i18next';
export interface Props {
list?: TokenItemProps['item'][];
isSearch: boolean;
onFocusInput: () => void;
onOpenAddEntryPopup: () => void;
isNoResults?: boolean;
blockedTokens?: TokenItemProps['item'][];
customizeTokens?: TokenItemProps['item'][];
isTestnet: boolean;
selectChainId?: string | null;
lpTokenMode?: boolean;
}
export const HomeTokenList = ({
list,
onFocusInput,
onOpenAddEntryPopup,
isSearch,
isNoResults,
blockedTokens,
customizeTokens,
isTestnet,
selectChainId,
lpTokenMode,
}) => {
export const HomeTokenList = ({ list, isSearch, isNoResults, lpTokenMode }) => {
const totalValue = React.useMemo(() => {
return list
?.reduce(
@@ -44,19 +25,8 @@ export const HomeTokenList = ({
}, [list]);
const { result: currentList } = useExpandList(list, totalValue);
const lowValueList = React.useMemo(() => {
// 排除customized tokens
const customizedTokenIds = new Set(
customizeTokens?.map((token) => token.id) || []
);
return list?.filter(
(item) =>
currentList?.indexOf(item) === -1 &&
!customizedTokenIds.has(item.id) &&
!blockedTokens?.some(
(blocked) => blocked.id === item.id && blocked.chain === item.chain
)
);
}, [currentList, list, isSearch, customizeTokens]);
return list?.filter((item) => currentList?.indexOf(item) === -1);
}, [currentList, list, isSearch]);
const { t } = useTranslation();
if (isNoResults) {
@@ -82,12 +52,7 @@ export const HomeTokenList = ({
);
}
const hasList = !!(
list?.length ||
currentList?.length ||
blockedTokens?.length ||
customizeTokens?.length
);
const hasList = !!(list?.length || currentList?.length);
const hasLowValueList = !!lowValueList?.length;
return (
@@ -101,20 +66,6 @@ export const HomeTokenList = ({
<TokenLowValueItem list={lowValueList} className="h-[48px]" />
)}
</div>
{/* {!isSearch && hasList && (
<div className="flex gap-12 pt-12 mt-[1px]">
<CustomizedButton
onClickButton={onOpenAddEntryPopup}
isTestnet={isTestnet}
selectChainId={selectChainId}
/>
<BlockedButton
onClickLink={onFocusInput}
isTestnet={isTestnet}
selectChainId={selectChainId}
/>
</div>
)} */}
</div>
);
};

View File

@@ -6,23 +6,20 @@ import React, {
useCallback,
useRef,
useMemo,
useImperativeHandle,
} from 'react';
import useCurrentBalance from '@/ui/hooks/useCurrentBalance';
import { formatUsdValue, useCommonPopupView, useWallet } from 'ui/utils';
import { useCommonPopupView, useWallet } from 'ui/utils';
import { KEYRING_TYPE } from 'consts';
import { SvgIconOffline } from '@/ui/assets';
import clsx from 'clsx';
import { Skeleton } from 'antd';
import { Chain } from '@debank/common';
import { ChainList } from './ChainList';
import { formChartData, useCurve } from './useCurve';
import { CurvePoint, CurveThumbnail } from './CurveView';
import ArrowNextSVG from '@/ui/assets/dashboard/arrow-next.svg';
import { ReactComponent as UpdateSVG } from '@/ui/assets/dashboard/update.svg';
import { ReactComponent as WarningSVG } from '@/ui/assets/dashboard/warning-1.svg';
import { useDebounce } from 'react-use';
import { useRabbyDispatch, useRabbySelector } from '@/ui/store';
import { useRabbySelector } from '@/ui/store';
import { BalanceLabel } from './BalanceLabel';
import { useTranslation } from 'react-i18next';
import { TooltipWithMagnetArrow } from '@/ui/component/Tooltip/TooltipWithMagnetArrow';
@@ -166,16 +163,10 @@ export const BalanceView = ({
refreshCurve,
isExpired: getCacheExpired,
});
const { refreshPositions } = useQueryProjects(
currentAccount?.address,
false,
true,
false,
false,
false,
false,
false
);
const { refreshPositions } = useQueryProjects(currentAccount?.address, {
visible: true,
autoLoad: false,
});
// const refreshTimerlegacy = useRef<NodeJS.Timeout>();
// only execute once on component mounted or address changed

View File

@@ -44,8 +44,6 @@ const PAGE_COUNT = 10;
interface TokenDetailProps {
onClose?(): void;
token: TokenItem;
addToken(token: TokenItem): void;
removeToken(token: TokenItem): void;
variant?: 'add';
isAdded?: boolean;
canClickToken?: boolean;
@@ -57,8 +55,6 @@ interface TokenDetailProps {
const TokenDetail = ({
token,
addToken,
removeToken,
variant,
isAdded,
onClose,

View File

@@ -6,8 +6,6 @@ import './style.less';
import { getUiType, isSameAddress, useWallet } from '@/ui/utils';
import { Account, Token } from '@/background/service/preference';
import { useRabbyDispatch } from 'ui/store';
import { DisplayedToken } from 'ui/utils/portfolio/project';
import { AbstractPortfolioToken } from 'ui/utils/portfolio/types';
import { useLocation } from 'react-router-dom';
import { DrawerProps } from 'antd';
@@ -55,36 +53,6 @@ export const TokenDetailPopup = ({
const isInSend = location.pathname === '/send-token';
const isBridge = location.pathname === '/bridge';
const handleAddToken = React.useCallback((tokenWithAmount) => {
if (!tokenWithAmount) return;
if (tokenWithAmount.is_core) {
dispatch.account.addBlockedToken(
new DisplayedToken(tokenWithAmount) as AbstractPortfolioToken
);
} else {
dispatch.account.addCustomizeToken(
new DisplayedToken(tokenWithAmount) as AbstractPortfolioToken
);
}
setIsAdded(true);
}, []);
const handleRemoveToken = React.useCallback((tokenWithAmount) => {
if (!tokenWithAmount) return;
if (tokenWithAmount?.is_core) {
dispatch.account.removeBlockedToken(
new DisplayedToken(tokenWithAmount) as AbstractPortfolioToken
);
} else {
dispatch.account.removeCustomizeToken(
new DisplayedToken(tokenWithAmount) as AbstractPortfolioToken
);
}
setIsAdded(false);
}, []);
const checkIsAdded = React.useCallback(async () => {
if (!token) return;
@@ -123,8 +91,6 @@ export const TokenDetailPopup = ({
account={account}
token={token}
popupHeight={popupHeight}
addToken={handleAddToken}
removeToken={handleRemoveToken}
variant={variant}
isAdded={isAdded}
onClose={onClose}

View File

@@ -1,15 +1,12 @@
import { Modal, ModalProps } from 'antd';
import React, { useMemo } from 'react';
import { Modal } from 'antd';
import React from 'react';
import TokenDetail from '../../../Dashboard/components/TokenDetailPopup/TokenDetail';
import { TokenItem } from '@/background/service/openapi';
import { Account, Token } from '@/background/service/preference';
import { Token } from '@/background/service/preference';
import { useCurrentAccount } from '@/ui/hooks/backgroundState/useAccount';
import { PopupContainer } from '@/ui/hooks/usePopupContainer';
import { SvgIconCross } from 'ui/assets';
import { isSameAddress, useWallet } from '@/ui/utils';
import { useRabbyDispatch } from '@/ui/store';
import { DisplayedToken } from '@/ui/utils/portfolio/project';
import { AbstractPortfolioToken } from '@/ui/utils/portfolio/types';
interface TokenDetailModalProps {
visible?: boolean;
onClose?(): void;
@@ -30,39 +27,8 @@ export const TokenDetailModal: React.FC<TokenDetailModalProps> = ({
}) => {
const account = useCurrentAccount();
const wallet = useWallet();
const dispatch = useRabbyDispatch();
const [isAdded, setIsAdded] = React.useState(false);
const handleAddToken = React.useCallback((tokenWithAmount) => {
if (!tokenWithAmount) return;
if (tokenWithAmount.is_core) {
dispatch.account.addBlockedToken(
new DisplayedToken(tokenWithAmount) as AbstractPortfolioToken
);
} else {
dispatch.account.addCustomizeToken(
new DisplayedToken(tokenWithAmount) as AbstractPortfolioToken
);
}
setIsAdded(true);
}, []);
const handleRemoveToken = React.useCallback((tokenWithAmount) => {
if (!tokenWithAmount) return;
if (tokenWithAmount?.is_core) {
dispatch.account.removeBlockedToken(
new DisplayedToken(tokenWithAmount) as AbstractPortfolioToken
);
} else {
dispatch.account.removeCustomizeToken(
new DisplayedToken(tokenWithAmount) as AbstractPortfolioToken
);
}
setIsAdded(false);
}, []);
const checkIsAdded = React.useCallback(async () => {
if (!token) return;
@@ -108,8 +74,6 @@ export const TokenDetailModal: React.FC<TokenDetailModalProps> = ({
<PopupContainer className="h-[600px] bg-r-neutral-bg-2">
<TokenDetail
account={account || undefined}
addToken={handleAddToken}
removeToken={handleRemoveToken}
variant="add"
isAdded={isAdded}
token={token}

View File

@@ -1,11 +1,7 @@
import React, { useEffect } from 'react';
import ProtocolList from './ProtocolList';
import { useTranslation } from 'react-i18next';
import { useRabbyDispatch } from '@/ui/store';
import {
AbstractPortfolioToken,
AbstractProject,
} from '@/ui/utils/portfolio/types';
import { AbstractProject } from '@/ui/utils/portfolio/types';
import { useExpandList } from './useExpandList';
import ProjectOverview from './ProjectOverview';
import { TokenListEmpty } from './TokenListEmpty';
@@ -41,11 +37,6 @@ export const DIFITab = ({
onProjectOverviewListChange,
}: Props) => {
const { t } = useTranslation();
const dispatch = useRabbyDispatch();
const setAllMode = (value: boolean) => {
dispatch.preference.setDesktopTokensAllMode(value);
};
const {
isExpanded,

View File

@@ -5,7 +5,7 @@ import { useCommonPopupView } from '@/ui/utils';
import { useQueryProjects } from '@/ui/utils/portfolio';
import { useEffect, useMemo, useState } from 'react';
export const useTokenAndDIFIData = ({
export const useTokenAndDefiData = ({
selectChainId,
allTokenMode,
}: {
@@ -32,21 +32,17 @@ export const useTokenAndDIFIData = ({
refreshPositions,
refreshTokens,
refreshPortfolios,
} = useQueryProjects(
currentAccount?.address,
false,
true,
false,
} = useQueryProjects(currentAccount?.address, {
visible: true,
lpTokenMode,
true,
allTokenMode
);
searchMode: allTokenMode,
});
const {
data: appPortfolios,
netWorth: appPortfolioNetWorth,
isLoading: isAppPortfoliosLoading,
} = useAppChain(currentAccount?.address, true, false);
} = useAppChain(currentAccount?.address, true);
const currentPortfolioNetWorth = useMemo(() => {
return (portfolioNetWorth || 0) + (appPortfolioNetWorth || 0);

View File

@@ -24,10 +24,7 @@ import { GnosisQueueModal } from './components/GnosisQueueModal';
import { ApprovalsTabPane } from './components/ApprovalsTabPane';
import { AddressDetailModal } from './components/AddressDetailModal';
import { AddressBackupModal } from './components/AddressBackupModal';
import { AddAddressModal } from './components/AddAddressModal';
import { RcIconBackTopCC } from '@/ui/assets/desktop/profile';
import { ReachedEnd } from './components/ReachedEnd';
import ThemeIcon from '@/ui/component/ThemeMode/ThemeIcon';
import TopShortcut, {
PORTFOLIO_LIST_ID,
TOP_SHORTCUT_SLOT_ID,
@@ -35,14 +32,11 @@ import TopShortcut, {
import { AbstractProject } from '@/ui/utils/portfolio/types';
import { NFTTabPane } from './components/NFTTabPane';
import { useEventBusListener } from '@/ui/hooks/useEventBusListener';
import { matomoRequestEvent } from '@/utils/matomo-request';
import { ga4 } from '@/utils/ga4';
import { DesktopPending } from './components/DesktopPending';
import { TokenTab } from './components/TokensTabPane/TokenTab';
import { DIFITab } from './components/TokensTabPane/DifiTab';
import { useTokenAndDIFIData } from './components/TokensTabPane/hook';
import { useTokenAndDefiData } from './components/TokensTabPane/hook';
import { DesktopPageWrap } from '@/ui/component/DesktopPageWrap';
import { SwitchThemeBtn } from './components/SwitchThemeBtn';
const DESKTOP_NAV_HEIGHT = 0;
const StickyBorderTop = () => (
@@ -137,23 +131,13 @@ export const DesktopProfile: React.FC<{
const [searchValue, setSearchValue] = React.useState('');
const {
// useQueryProjects
isTokensLoading,
isAllTokenLoading,
isPortfoliosLoading,
portfolios,
tokenList,
hasTokens,
removeProtocol,
portfolioNetWorth,
// useQueryProjects end
// useAppChain
appPortfolios,
appPortfolioNetWorth,
isAppPortfoliosLoading,
// useAppChain end
currentPortfolioNetWorth,
displayTokenList,
displayPortfolios,
sortTokens,
lpTokenMode,
@@ -162,7 +146,7 @@ export const DesktopProfile: React.FC<{
isNoResults,
refreshPositions,
refreshTokens,
} = useTokenAndDIFIData({
} = useTokenAndDefiData({
selectChainId: chainInfo?.serverId,
allTokenMode: !!searchValue,
});
@@ -279,7 +263,7 @@ export const DesktopProfile: React.FC<{
}
isNoResults={isNoResults}
sortTokens={sortTokens}
hasTokens={hasTokens}
hasTokens={!!hasTokens}
lpTokenMode={lpTokenMode}
setLpTokenMode={setLpTokenMode}
selectChainId={chainInfo?.serverId}

View File

@@ -115,19 +115,10 @@ const DesktopSmallSwapContent: React.FC = () => {
tokens: allTokens,
isLoading: isLoadingAllTokens,
updateData: updateAllTokens,
} = useTokens(
chainServerId ? currentAccount?.address : undefined,
undefined,
true,
undefined,
} = useTokens(chainServerId ? currentAccount?.address : undefined, {
chainServerId,
undefined,
undefined,
undefined,
false,
false,
true
);
realtimeMode: true,
});
const isSupportDB = isSupportDBAccount(currentAccount);