Skip to content

Commit

Permalink
Merge pull request #3829 from Koniverse/koni/dev/issue-3132
Browse files Browse the repository at this point in the history
[Issue-3132] Improve chainlist online patch
  • Loading branch information
saltict authored Nov 22, 2024
2 parents 2bf7bb2 + 222a14d commit 4962a70
Show file tree
Hide file tree
Showing 10 changed files with 388 additions and 119 deletions.
1 change: 1 addition & 0 deletions packages/extension-base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
"protobufjs": "^7.2.4",
"rxjs": "^7.8.1",
"sails-js": "^0.1.6",
"ts-md5": "^1.3.1",
"tweetnacl": "^1.0.3",
"uuid": "^9.0.0",
"web3": "^1.10.0",
Expand Down
17 changes: 13 additions & 4 deletions packages/extension-base/src/koni/background/handlers/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { BalanceService } from '@subwallet/extension-base/services/balance-servi
import { ServiceStatus } from '@subwallet/extension-base/services/base/types';
import BuyService from '@subwallet/extension-base/services/buy-service';
import CampaignService from '@subwallet/extension-base/services/campaign-service';
import { ChainOnlineService } from '@subwallet/extension-base/services/chain-online-service';
import { ChainService } from '@subwallet/extension-base/services/chain-service';
import { _DEFAULT_MANTA_ZK_CHAIN, _MANTA_ZK_CHAIN_GROUP, _PREDEFINED_SINGLE_MODES } from '@subwallet/extension-base/services/chain-service/constants';
import { _ChainState, _NetworkUpsertParams, _ValidateCustomAssetRequest } from '@subwallet/extension-base/services/chain-service/types';
Expand Down Expand Up @@ -132,6 +133,7 @@ export default class KoniState {
readonly feeService: FeeService;
readonly swapService: SwapService;
readonly inappNotificationService: InappNotificationService;
readonly chainOnlineService: ChainOnlineService;

// Handle the general status of the extension
private generalStatus: ServiceStatus = ServiceStatus.INITIALIZING;
Expand Down Expand Up @@ -165,6 +167,7 @@ export default class KoniState {
this.feeService = new FeeService(this);
this.swapService = new SwapService(this);
this.inappNotificationService = new InappNotificationService(this.dbService, this.keyringService, this.eventService, this.chainService);
this.chainOnlineService = new ChainOnlineService(this.chainService, this.settingService, this.eventService, this.dbService);

this.subscription = new KoniSubscription(this, this.dbService);
this.cron = new KoniCron(this, this.subscription, this.dbService);
Expand Down Expand Up @@ -282,7 +285,6 @@ export default class KoniState {
public async init () {
await this.eventService.waitCryptoReady;
await this.chainService.init();
this.afterChainServiceInit();
await this.migrationService.run();
this.campaignService.init();
this.mktCampaignService.init();
Expand All @@ -301,8 +303,11 @@ export default class KoniState {
await this.dbService.stores.crowdloan.removeEndedCrowdloans();

await this.startSubscription();

this.chainOnlineService.checkLatestData();
this.chainService.checkLatestData();
this.chainService.subscribeChainInfoMap().subscribe(() => {
this.afterChainServiceInit();
});
}

public async initMantaPay (password: string) {
Expand Down Expand Up @@ -1002,6 +1007,8 @@ export default class KoniState {
}

async resumeAllNetworks () {
this.chainOnlineService.checkLatestData();

return this.chainService.resumeAllChainApis();
}

Expand Down Expand Up @@ -1652,9 +1659,11 @@ export default class KoniState {
await this.walletConnectService.resetWallet(resetAll);

await this.chainService.init();
this.afterChainServiceInit();

this.chainOnlineService.checkLatestData();
this.chainService.checkLatestData();
this.chainService.subscribeChainInfoMap().subscribe(() => {
this.afterChainServiceInit();
});
}

public async enableMantaPay (updateStore: boolean, address: string, password: string, seedPhrase?: string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright 2019-2022 @subwallet/extension-koni authors & contributors
// SPDX-License-Identifier: Apache-2.0

export const LATEST_CHAIN_PATCH_FETCHING_INTERVAL = 180000;
253 changes: 253 additions & 0 deletions packages/extension-base/src/services/chain-online-service/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
// Copyright 2019-2022 @subwallet/extension-koni authors & contributors
// SPDX-License-Identifier: Apache-2.0

import { AssetLogoMap, ChainLogoMap, MultiChainAssetMap } from '@subwallet/chain-list';
import { _ChainAsset, _ChainInfo, _MultiChainAsset } from '@subwallet/chain-list/types';
import { LATEST_CHAIN_PATCH_FETCHING_INTERVAL } from '@subwallet/extension-base/services/chain-online-service/constants';
import { ChainService, filterAssetInfoMap } from '@subwallet/extension-base/services/chain-service';
import { _ChainApiStatus, _ChainConnectionStatus, _ChainState } from '@subwallet/extension-base/services/chain-service/types';
import { fetchPatchData, PatchInfo, randomizeProvider } from '@subwallet/extension-base/services/chain-service/utils';
import { EventService } from '@subwallet/extension-base/services/event-service';
import SettingService from '@subwallet/extension-base/services/setting-service/SettingService';
import { IChain } from '@subwallet/extension-base/services/storage-service/databases';
import DatabaseService from '@subwallet/extension-base/services/storage-service/DatabaseService';
import { Md5 } from 'ts-md5';

export class ChainOnlineService {
private chainService: ChainService;
private settingService: SettingService;
private eventService: EventService;
private dbService: DatabaseService;

refreshLatestChainDataTimeOut: NodeJS.Timer | undefined;

constructor (chainService: ChainService, settingService: SettingService, eventService: EventService, dbService: DatabaseService) {
this.chainService = chainService;
this.settingService = settingService;
this.eventService = eventService;
this.dbService = dbService;
}

md5Hash (data: any) {
return Md5.hashStr(JSON.stringify(data));
}

validatePatchWithHash (latestPatch: PatchInfo) {
const { ChainAsset, ChainAssetHashMap, ChainInfo, ChainInfoHashMap, MultiChainAsset, MultiChainAssetHashMap } = latestPatch;

for (const [chainSlug, chain] of Object.entries(ChainInfo)) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { chainStatus, providers, ...chainWithoutProvidersAndStatus } = chain;

if (this.md5Hash(chainWithoutProvidersAndStatus) !== ChainInfoHashMap[chainSlug]) {
return false;
}
}

for (const [assetSlug, asset] of Object.entries(ChainAsset)) {
if (this.md5Hash(asset) !== ChainAssetHashMap[assetSlug]) {
return false;
}
}

for (const [mAssetSlug, mAsset] of Object.entries(MultiChainAsset)) {
if (this.md5Hash(mAsset) !== MultiChainAssetHashMap[mAssetSlug]) {
return false;
}
}

return true;
}

validatePatchBeforeStore (candidateChainInfoMap: Record<string, _ChainInfo>, candidateAssetRegistry: Record<string, _ChainAsset>, candidateMultiChainAssetMap: Record<string, _MultiChainAsset>, latestPatch: PatchInfo) {
for (const [chainSlug, chainHash] of Object.entries(latestPatch.ChainInfoHashMap)) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { chainStatus, providers, ...chainWithoutProvidersAndStatus } = candidateChainInfoMap[chainSlug];

if (this.md5Hash(chainWithoutProvidersAndStatus) !== chainHash) {
return false;
}
}

for (const [assetSlug, assetHash] of Object.entries(latestPatch.ChainAssetHashMap)) {
if (!candidateAssetRegistry[assetSlug]) {
if (!latestPatch.ChainInfo[assetSlug]) { // assets are not existed in case chain is removed
continue;
}

return false;
}

if (this.md5Hash(candidateAssetRegistry[assetSlug]) !== assetHash) {
return false;
}
}

for (const [mAssetSlug, mAssetHash] of Object.entries(latestPatch.MultiChainAssetHashMap)) {
if (this.md5Hash(candidateMultiChainAssetMap[mAssetSlug]) !== mAssetHash) {
return false;
}
}

return true;
}

async handleLatestPatch (latestPatch: PatchInfo) {
try {
// 1. validate fetch data with its hash
const isSafePatch = this.validatePatchWithHash(latestPatch);
const { AssetLogoMap: latestAssetLogoMap,
ChainAsset: latestAssetInfo,
ChainInfo: latestChainInfo,
ChainLogoMap: latestChainLogoMap,
MultiChainAsset: latestMultiChainAsset,
mAssetLogoMap: lastestMAssetLogoMap,
patchVersion: latestPatchVersion } = latestPatch;
const currentPatchVersion = (await this.settingService.getChainlistSetting())?.patchVersion || '';

let chainInfoMap: Record<string, _ChainInfo> = structuredClone(this.chainService.getChainInfoMap());
let assetRegistry: Record<string, _ChainAsset> = structuredClone(this.chainService.getAssetRegistry());
let multiChainAssetMap: Record<string, _MultiChainAsset> = structuredClone(MultiChainAssetMap);
let currentChainStateMap: Record<string, _ChainState> = structuredClone(this.chainService.getChainStateMap());
let currentChainStatusMap: Record<string, _ChainApiStatus> = structuredClone(this.chainService.getChainStatusMap());
let addedChain: string[] = [];

if (isSafePatch && currentPatchVersion !== latestPatchVersion) {
// 2. merge data map
if (latestChainInfo && Object.keys(latestChainInfo).length > 0) {
chainInfoMap = Object.assign({}, this.chainService.getChainInfoMap(), latestChainInfo);

[currentChainStateMap, currentChainStatusMap] = [structuredClone(this.chainService.getChainStateMap()), structuredClone(this.chainService.getChainStatusMap())];

const [currentChainStateKey, newChainKey] = [Object.keys(currentChainStateMap), Object.keys(chainInfoMap)];

addedChain = newChainKey.filter((chain) => !currentChainStateKey.includes(chain));

addedChain.forEach((key) => {
currentChainStateMap[key] = {
active: false,
currentProvider: randomizeProvider(chainInfoMap[key].providers).providerKey,
manualTurnOff: false,
slug: key
};

currentChainStatusMap[key] = {
slug: key,
connectionStatus: _ChainConnectionStatus.DISCONNECTED,
lastUpdated: Date.now()
};
});
}

if (latestAssetInfo && Object.keys(latestAssetInfo).length > 0) {
assetRegistry = filterAssetInfoMap(this.chainService.getChainInfoMap(), Object.assign({}, this.chainService.getAssetRegistry(), latestAssetInfo), addedChain);
}

if (latestMultiChainAsset && Object.keys(latestMultiChainAsset).length > 0) { // todo: currently not support update latest multichain-asset
multiChainAssetMap = { ...MultiChainAssetMap, ...latestMultiChainAsset };
}

// 3. validate data before write
const isCorrectPatch = this.validatePatchBeforeStore(chainInfoMap, assetRegistry, multiChainAssetMap, latestPatch);

// 4. write to subject
if (isCorrectPatch) {
this.chainService.setChainInfoMap(chainInfoMap);
this.chainService.subscribeChainInfoMap().next(chainInfoMap);

this.chainService.setAssetRegistry(assetRegistry);
this.chainService.subscribeAssetRegistry().next(assetRegistry);
this.chainService.autoEnableTokens()
.then(() => {
this.eventService.emit('asset.updateState', '');
})
.catch(console.error);

this.chainService.subscribeMultiChainAssetMap().next(multiChainAssetMap);

this.chainService.setChainStateMap(currentChainStateMap);
this.chainService.subscribeChainStateMap().next(currentChainStateMap);

this.chainService.subscribeChainStatusMap().next(currentChainStatusMap);

const storedChainInfoList: IChain[] = Object.keys(chainInfoMap).map((chainSlug) => {
return {
...chainInfoMap[chainSlug],
...currentChainStateMap[chainSlug]
};
});

await this.dbService.bulkUpdateChainStore(storedChainInfoList);

const addedAssets: _ChainAsset[] = [];

Object.entries(assetRegistry).forEach(([slug, asset]) => {
if (addedChain.includes(asset.originChain)) {
addedAssets.push(asset);
}
});

await this.dbService.bulkUpdateAssetsStore(addedAssets);

if (latestChainLogoMap) {
const logoMap = Object.assign({}, ChainLogoMap, latestChainLogoMap);

this.chainService.subscribeChainLogoMap().next(logoMap);
}

if (latestAssetLogoMap) {
const logoMap = Object.assign({}, AssetLogoMap, latestAssetLogoMap, lastestMAssetLogoMap);

this.chainService.subscribeAssetLogoMap().next(logoMap);
}

this.settingService.setChainlist({ patchVersion: latestPatchVersion });
}
}
} catch (e) {
console.error('Error fetching latest patch data');
}
}

private async fetchLatestPatchData () {
return await fetchPatchData<PatchInfo>('data.json');
}

handleLatestPatchData () {
this.fetchLatestPatchData()
.then((latestPatch) => {
return new Promise<void>((resolve) => {
if (latestPatch && !this.chainService.getlockChainInfoMap()) {
this.eventService.waitAssetReady
.then(() => {
this.chainService.setLockChainInfoMap(true);
this.handleLatestPatch(latestPatch)
.then(() => this.chainService.setLockChainInfoMap(false))
.catch((e) => {
this.chainService.setLockChainInfoMap(false);
console.error('Error update latest patch', e);
})
.finally(resolve);
})
.catch((e) => {
console.error('Asset fail to ready', e);
resolve();
});
} else {
resolve();
}
});
}).catch((e) => {
console.error('Error get latest patch or data map is locking', e);
}).finally(() => {
this.eventService.emit('asset.online.ready', true);
});
}

checkLatestData () {
clearInterval(this.refreshLatestChainDataTimeOut);
this.handleLatestPatchData();

this.refreshLatestChainDataTimeOut = setInterval(this.handleLatestPatchData.bind(this), LATEST_CHAIN_PATCH_FETCHING_INTERVAL);
}
}
Loading

1 comment on commit 4962a70

@saltict
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.