Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ export interface ISimpleDBAggregateToken {
}
>;
allAggregateTokens?: IAccountToken[];
// Tracks the last successful wallet-config sync so stale caches written by
// older app versions (with a different preset network list) get refreshed.
configSyncMeta?: {
appVersion: string;
syncedAt: number;
};
tokenDetails?: Record<
string, // all networks accountId
Record<
Expand Down Expand Up @@ -186,17 +192,23 @@ export class SimpleDbEntityAggregateToken extends SimpleDbEntityBase<ISimpleDBAg
homeDefaultTokenMap,
allAggregateTokenMap,
allAggregateTokens,
configSyncMeta,
merge = false,
}: {
allAggregateTokenMap: Record<string, { tokens: IAccountToken[] }>;
allAggregateTokens: IAccountToken[];
aggregateTokenConfigMap: Record<string, IAggregateToken>;
aggregateTokenSymbolMap: Record<string, boolean>;
homeDefaultTokenMap: Record<string, IHomeDefaultToken>;
configSyncMeta?: {
appVersion: string;
syncedAt: number;
};
merge?: boolean;
}) {
await this.setRawData((rawData) => ({
...rawData,
configSyncMeta: configSyncMeta ?? rawData?.configSyncMeta,
aggregateTokenSymbolMap: merge
? { ...rawData?.aggregateTokenSymbolMap, ...aggregateTokenSymbolMap }
: aggregateTokenSymbolMap,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return */

import platformEnv from '@onekeyhq/shared/src/platformEnv';
import timerUtils from '@onekeyhq/shared/src/utils/timerUtils';

import ServiceSetting from './ServiceSetting';

import type { ISimpleDBAggregateToken } from '../dbs/simple/entity/SimpleDbEntityAggregateToken';

jest.mock('@onekeyhq/shared/src/background/backgroundDecorators', () => ({
backgroundClass: () => (target: any) => target,
backgroundMethod: () => (_t: any, _k: string, desc: any) => desc,
backgroundMethodForDev: () => (_t: any, _k: string, desc: any) => desc,
toastIfError: () => (_t: any, _k: string, desc: any) => desc,
checkDevOnlyPassword: jest.fn(),
}));

const currentAppVersion = platformEnv.version ?? '';
const oneDayMs = timerUtils.getTimeDurationMs({ day: 1 });

function buildService(rawData: ISimpleDBAggregateToken | null) {
const service = new ServiceSetting({
backgroundApi: {
simpleDb: {
aggregateToken: {
getRawData: jest.fn(async () => rawData),
},
},
},
});
const syncSpy = jest
.spyOn(service, 'syncWalletConfig')
.mockResolvedValue({} as any);
return { service, syncSpy };
}

describe('ServiceSetting.syncWalletConfigIfNeeded', () => {
it('syncs when the config has never been synced', async () => {
const { service, syncSpy } = buildService(null);

await service.syncWalletConfigIfNeeded();

expect(syncSpy).toHaveBeenCalledTimes(1);
});

it('syncs when the cached config has no sync meta', async () => {
const { service, syncSpy } = buildService({
aggregateTokenConfigMap: {},
});

await service.syncWalletConfigIfNeeded();

expect(syncSpy).toHaveBeenCalledTimes(1);
});

it('syncs when the app version changed since the last sync', async () => {
const { service, syncSpy } = buildService({
aggregateTokenConfigMap: {},
configSyncMeta: {
appVersion: 'stale-app-version',
syncedAt: Date.now(),
},
});

await service.syncWalletConfigIfNeeded();

expect(syncSpy).toHaveBeenCalledTimes(1);
});

it('syncs when the cached config is older than the TTL', async () => {
const { service, syncSpy } = buildService({
aggregateTokenConfigMap: {},
configSyncMeta: {
appVersion: currentAppVersion,
syncedAt: Date.now() - oneDayMs - 1,
},
});

await service.syncWalletConfigIfNeeded();

expect(syncSpy).toHaveBeenCalledTimes(1);
});

it('skips syncing when the cached config is fresh', async () => {
const { service, syncSpy } = buildService({
aggregateTokenConfigMap: {},
configSyncMeta: {
appVersion: currentAppVersion,
syncedAt: Date.now(),
},
});

await service.syncWalletConfigIfNeeded();

expect(syncSpy).not.toHaveBeenCalled();
});

it('dedupes concurrent calls into a single sync', async () => {
const { service, syncSpy } = buildService(null);

await Promise.all([
service.syncWalletConfigIfNeeded(),
service.syncWalletConfigIfNeeded(),
]);

expect(syncSpy).toHaveBeenCalledTimes(1);
});
});
35 changes: 35 additions & 0 deletions packages/kit-bg/src/services/ServiceSetting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,10 @@ class ServiceSetting extends ServiceBase {
homeDefaultTokenMap,
allAggregateTokenMap,
aggregateTokenSymbolMap,
configSyncMeta: {
appVersion: platformEnv.version ?? '',
syncedAt: Date.now(),
},
}),
this.backgroundApi.simpleDb.approval.updateApprovalResurfaceDaysConfig({
approvalResurfaceDays,
Expand All @@ -1000,6 +1004,37 @@ class ServiceSetting extends ServiceBase {
return aggregateTokenConfigMap;
}

_syncWalletConfigIfNeededPromise: Promise<void> | undefined;

@backgroundMethod()
public async syncWalletConfigIfNeeded() {
if (this._syncWalletConfigIfNeededPromise) {
return this._syncWalletConfigIfNeededPromise;
}
this._syncWalletConfigIfNeededPromise = (async () => {
const rawData =
await this.backgroundApi.simpleDb.aggregateToken.getRawData();
const configSyncMeta = rawData?.configSyncMeta;
const appVersion = platformEnv.version ?? '';
// Re-sync when the config has never been synced, when the app version
// changed (the bundled preset network list may differ, leaving stale
// networks in the cached aggregate-token maps), or when the cache is
// older than the TTL.
const shouldSync =
!rawData?.aggregateTokenConfigMap ||
!configSyncMeta ||
configSyncMeta.appVersion !== appVersion ||
Date.now() - configSyncMeta.syncedAt >
timerUtils.getTimeDurationMs({ day: 1 });
if (shouldSync) {
await this.syncWalletConfig();
}
})().finally(() => {
this._syncWalletConfigIfNeededPromise = undefined;
});
return this._syncWalletConfigIfNeededPromise;
}

@backgroundMethod()
async isKytIntroShown({ onekeyUserId }: { onekeyUserId: string }) {
if (!onekeyUserId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return */

import ServiceToken from './ServiceToken';

import type { ISimpleDBAggregateToken } from '../dbs/simple/entity/SimpleDbEntityAggregateToken';

jest.mock('@onekeyhq/shared/src/background/backgroundDecorators', () => ({
backgroundClass: () => (target: any) => target,
backgroundMethod: () => (_t: any, _k: string, desc: any) => desc,
backgroundMethodForDev: () => (_t: any, _k: string, desc: any) => desc,
toastIfError: () => (_t: any, _k: string, desc: any) => desc,
checkDevOnlyPassword: jest.fn(),
}));

function buildService(rawData: ISimpleDBAggregateToken | null) {
return new ServiceToken({
backgroundApi: {
simpleDb: {
aggregateToken: {
getRawData: jest.fn(async () => rawData),
},
},
},
});
}

function buildToken(networkId: string) {
return {
$key: `sameSymbol_${networkId}`,
networkId,
name: 'Tether',
symbol: 'USDT',
address: '0xusdt',
decimals: 6,
isNative: false,
};
}

// Mirrors the aggregate descriptor shape built by
// ServiceSetting.syncWalletConfig from the map keys.
function buildAggregateToken($key: string) {
return {
$key,
isAggregateToken: true,
name: 'Tether',
symbol: 'USDT',
commonSymbol: 'USDT',
networkId: '',
address: $key,
decimals: 0,
isNative: false,
};
}

describe('ServiceToken.getAllAggregateTokenInfo', () => {
it('drops tokens on networks missing from the bundled preset list', async () => {
// evm--810180 (zkLink Nova) was removed from presetNetworks and
// evm--321 (KCC) never existed there; both may still linger in an
// aggregate-token cache persisted by an older app version.
const service = buildService({
allAggregateTokenMap: {
sameSymbol_USDT: {
tokens: [
buildToken('evm--1'),
buildToken('evm--810180'),
buildToken('evm--321'),
],
},
},
allAggregateTokens: [],
});

const { allAggregateTokenMap } = await service.getAllAggregateTokenInfo();

expect(
allAggregateTokenMap.sameSymbol_USDT.tokens.map((t) => t.networkId),
).toEqual(['evm--1']);
});

it('keeps tokens on bundled listed networks intact', async () => {
const service = buildService({
allAggregateTokenMap: {
sameSymbol_USDT: {
tokens: [buildToken('evm--1'), buildToken('tron--0x2b6653dc')],
},
},
allAggregateTokens: [],
});

const { allAggregateTokenMap } = await service.getAllAggregateTokenInfo();

expect(
allAggregateTokenMap.sameSymbol_USDT.tokens.map((t) => t.networkId),
).toEqual(['evm--1', 'tron--0x2b6653dc']);
});

it('drops aggregate groups whose networks were all delisted', async () => {
// evm--100 (Gnosis) was removed from presetNetworks, so the whole
// XDAI aggregate group becomes empty after filtering and must be
// dropped from both the map and the flat descriptor list.
const service = buildService({
allAggregateTokenMap: {
sameSymbol_USDT: {
tokens: [buildToken('evm--1'), buildToken('evm--810180')],
},
sameSymbol_XDAI: {
tokens: [buildToken('evm--100')],
},
},
allAggregateTokens: [
buildAggregateToken('sameSymbol_USDT'),
buildAggregateToken('sameSymbol_XDAI'),
],
});

const { allAggregateTokenMap, allAggregateTokens } =
await service.getAllAggregateTokenInfo();

expect(Object.keys(allAggregateTokenMap)).toEqual(['sameSymbol_USDT']);
expect(allAggregateTokens.map((t) => t.$key)).toEqual(['sameSymbol_USDT']);
});

it('returns empty structures when nothing is cached', async () => {
const service = buildService(null);

const result = await service.getAllAggregateTokenInfo();

expect(result).toEqual({
allAggregateTokenMap: {},
allAggregateTokens: [],
});
});
});
27 changes: 24 additions & 3 deletions packages/kit-bg/src/services/ServiceToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import {
backgroundClass,
backgroundMethod,
} from '@onekeyhq/shared/src/background/backgroundDecorators';
import { getNetworkIdsMap } from '@onekeyhq/shared/src/config/networkIds';
import {
getListedNetworkMap,
getNetworkIdsMap,
} from '@onekeyhq/shared/src/config/networkIds';
import { USD_CURRENCY_ID } from '@onekeyhq/shared/src/consts/currencyConsts';
import { AGGREGATE_TOKEN_MOCK_NETWORK_ID } from '@onekeyhq/shared/src/consts/networkConsts';
import {
Expand Down Expand Up @@ -1383,9 +1386,27 @@ class ServiceToken extends ServiceBase {
public async getAllAggregateTokenInfo() {
const rawData =
await this.backgroundApi.simpleDb.aggregateToken.getRawData();
// Drop tokens on networks this build no longer bundles: the cached wallet
// config may have been persisted by an older app version whose preset
// network list included networks that were delisted since.
const listedNetworkMap = getListedNetworkMap();
const allAggregateTokenMap: Record<string, { tokens: IAccountToken[] }> =
{};
Object.entries(rawData?.allAggregateTokenMap ?? {}).forEach(
([key, value]) => {
const tokens = value.tokens.filter(
(token) => token.networkId && listedNetworkMap[token.networkId],
Comment thread
weatherstar marked this conversation as resolved.
);
if (tokens.length > 0) {
Comment thread
weatherstar marked this conversation as resolved.
allAggregateTokenMap[key] = { tokens };
}
},
);
return {
allAggregateTokenMap: rawData?.allAggregateTokenMap ?? {},
allAggregateTokens: rawData?.allAggregateTokens ?? [],
allAggregateTokenMap,
allAggregateTokens: (rawData?.allAggregateTokens ?? []).filter(
(token) => allAggregateTokenMap[token.$key]?.tokens.length,
),
};
}

Expand Down
1 change: 0 additions & 1 deletion packages/kit/src/hooks/useReceiveToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ function useReceiveToken({
if (withAllAggregateTokens) {
const res =
await backgroundApiProxy.serviceToken.getAllAggregateTokenInfo();
await backgroundApiProxy.serviceToken.getAllAggregateTokenInfo();
allAggregateTokenMap = res.allAggregateTokenMap;
allAggregateTokens = res.allAggregateTokens;
}
Expand Down
Loading
Loading