Skip to content

Commit a67f030

Browse files
committed
[feat] add dismissNewTokenFoundInHome
1 parent 83b14b5 commit a67f030

File tree

1 file changed

+240
-6
lines changed

1 file changed

+240
-6
lines changed

apps/extension/src/stores/chain/index.tsx

Lines changed: 240 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
toJS,
99
} from "mobx";
1010

11-
import { ChainInfo, ModularChainInfo } from "@keplr-wallet/types";
11+
import { AppCurrency, ChainInfo, ModularChainInfo } from "@keplr-wallet/types";
1212
import {
1313
ChainStore as BaseChainStore,
1414
IChainInfoImpl,
@@ -29,15 +29,22 @@ import {
2929
RemoveSuggestedChainInfoMsg,
3030
RevalidateTokenScansMsg,
3131
SetChainEndpointsMsg,
32+
SyncTokenScanInfosMsg,
3233
ToggleChainsMsg,
3334
TokenScan,
35+
TokenScanInfo,
3436
TryUpdateAllChainInfosMsg,
3537
TryUpdateEnabledChainInfosMsg,
3638
} from "@keplr-wallet/background";
3739
import { BACKGROUND_PORT, MessageRequester } from "@keplr-wallet/router";
3840
import { KVStore, toGenerator } from "@keplr-wallet/common";
3941
import { ChainIdHelper } from "@keplr-wallet/cosmos";
4042

43+
type Assets = {
44+
currency: AppCurrency;
45+
amount: string;
46+
};
47+
4148
export class ChainStore extends BaseChainStore<ChainInfoWithCoreTypes> {
4249
@observable
4350
protected _isInitializing: boolean = false;
@@ -53,6 +60,9 @@ export class ChainStore extends BaseChainStore<ChainInfoWithCoreTypes> {
5360
@observable
5461
protected _lastTokenScanRevalidateTimestamp: Map<string, number> = new Map();
5562

63+
@observable
64+
protected _newTokenFoundDismissed: Map<string, boolean> = new Map();
65+
5666
constructor(
5767
protected readonly kvStore: KVStore,
5868
protected readonly embedChainInfos: (ModularChainInfo | ChainInfo)[],
@@ -124,14 +134,217 @@ export class ChainStore extends BaseChainStore<ChainInfoWithCoreTypes> {
124134

125135
@computed
126136
get tokenScans(): TokenScan[] {
127-
return this._tokenScans.filter((scan) => {
128-
if (!this.hasChain(scan.chainId) && !this.hasModularChain(scan.chainId)) {
129-
return false;
137+
const resolveCurrency = (
138+
chainId: string,
139+
denom: string
140+
): AppCurrency | undefined => {
141+
const chainInfo = this.hasChain(chainId) ? this.getChain(chainId) : null;
142+
const modularChainInfo = this.hasModularChain(chainId)
143+
? this.getModularChain(chainId)
144+
: null;
145+
146+
const currencies: AppCurrency[] = (() => {
147+
if (chainInfo) return chainInfo.currencies;
148+
if (modularChainInfo) {
149+
if ("cosmos" in modularChainInfo) {
150+
return modularChainInfo.cosmos.currencies;
151+
}
152+
153+
if ("bitcoin" in modularChainInfo) {
154+
return modularChainInfo.bitcoin.currencies;
155+
}
156+
157+
if ("starknet" in modularChainInfo) {
158+
return modularChainInfo.starknet.currencies;
159+
}
160+
}
161+
return [];
162+
})();
163+
164+
if (chainInfo) {
165+
const found = chainInfo.forceFindCurrency(denom);
166+
if (!found.coinDenom.startsWith("ibc/")) {
167+
return found;
168+
}
169+
}
170+
171+
if (modularChainInfo) {
172+
const found = currencies.find((cur) => cur.coinMinimalDenom === denom);
173+
if (found) {
174+
return found;
175+
}
130176
}
131177

132-
const chainIdentifier = ChainIdHelper.parse(scan.chainId).identifier;
133-
return !this.enabledChainIdentifiesMap.get(chainIdentifier);
178+
return undefined;
179+
};
180+
181+
return this._tokenScans
182+
.filter((scan) => {
183+
if (
184+
!this.hasChain(scan.chainId) &&
185+
!this.hasModularChain(scan.chainId)
186+
) {
187+
return false;
188+
}
189+
190+
const chainIdentifier = ChainIdHelper.parse(scan.chainId).identifier;
191+
return !this.enabledChainIdentifiesMap.get(chainIdentifier);
192+
})
193+
.map((scan) => {
194+
const newInfos = scan.infos.map((info) => {
195+
const newAssets = info.assets
196+
.map((asset) => {
197+
const cur = resolveCurrency(
198+
scan.chainId,
199+
asset.currency.coinMinimalDenom
200+
);
201+
if (!cur) return undefined;
202+
return {
203+
...asset,
204+
currency: cur,
205+
};
206+
})
207+
.filter((a): a is Assets => !!a);
208+
209+
return {
210+
...info,
211+
assets: newAssets,
212+
};
213+
});
214+
215+
return {
216+
...scan,
217+
infos: newInfos,
218+
};
219+
});
220+
}
221+
222+
@computed
223+
get shouldShowNewTokenFoundInMain(): boolean {
224+
const vaultId = this.keyRingStore.selectedKeyInfo?.id;
225+
if (!vaultId) {
226+
return false;
227+
}
228+
229+
const dismissed = this._newTokenFoundDismissed.get(vaultId) ?? false;
230+
if (dismissed) {
231+
return false;
232+
}
233+
234+
return this.tokenScans.length > 0;
235+
}
236+
237+
dismissNewTokenFoundInHome() {
238+
const vaultId = this.keyRingStore.selectedKeyInfo?.id;
239+
if (!vaultId) {
240+
return;
241+
}
242+
243+
runInAction(() => {
244+
this._newTokenFoundDismissed.set(vaultId, true);
134245
});
246+
247+
// Sync prevInfos to current infos in background so future scans
248+
// compare against the state at dismiss time
249+
this.requester.sendMessage(
250+
BACKGROUND_PORT,
251+
new SyncTokenScanInfosMsg(vaultId)
252+
);
253+
}
254+
255+
protected resetDismissIfNeeded(vaultId: string, tokenScans: TokenScan[]) {
256+
const needReset = tokenScans.some((scan) =>
257+
this.isMeaningfulTokenScanChange(scan)
258+
);
259+
260+
if (needReset) {
261+
runInAction(() => {
262+
this._newTokenFoundDismissed.set(vaultId, false);
263+
});
264+
}
265+
}
266+
267+
protected isMeaningfulTokenScanChange(tokenScan: TokenScan): boolean {
268+
if (!tokenScan.prevInfos || tokenScan.prevInfos.length === 0) {
269+
return tokenScan.infos.length > 0;
270+
}
271+
272+
const makeKey = (info: TokenScanInfo): string | undefined => {
273+
if (info.bech32Address) return `bech32:${info.bech32Address}`;
274+
if (info.ethereumHexAddress) return `eth:${info.ethereumHexAddress}`;
275+
if (info.starknetHexAddress) return `stark:${info.starknetHexAddress}`;
276+
if (info.bitcoinAddress?.bech32Address)
277+
return `btc:${info.bitcoinAddress.bech32Address}`;
278+
if (info.coinType != null) return `coin:${info.coinType}`;
279+
return undefined;
280+
};
281+
282+
const toBigIntSafe = (v: string): bigint | undefined => {
283+
try {
284+
return BigInt(v);
285+
} catch {
286+
return undefined;
287+
}
288+
};
289+
290+
const prevTokenInfosMap = new Map<string, TokenScanInfo>();
291+
for (const info of tokenScan.prevInfos ?? []) {
292+
const key = makeKey(info);
293+
if (key) {
294+
prevTokenInfosMap.set(key, info);
295+
}
296+
}
297+
298+
for (const info of tokenScan.infos) {
299+
const key = makeKey(info);
300+
if (!key) {
301+
continue;
302+
}
303+
304+
const prevTokenInfo = prevTokenInfosMap.get(key);
305+
306+
if (!prevTokenInfo) {
307+
if (info.assets.length > 0) {
308+
return true;
309+
}
310+
continue;
311+
}
312+
313+
const prevAssetMap = new Map<string, Assets>();
314+
for (const asset of prevTokenInfo.assets) {
315+
prevAssetMap.set(asset.currency.coinMinimalDenom, asset);
316+
}
317+
318+
for (const asset of info.assets) {
319+
const prevAsset = prevAssetMap.get(asset.currency.coinMinimalDenom);
320+
321+
// 없던 토큰이 생긴경우
322+
if (!prevAsset) {
323+
return true;
324+
}
325+
326+
const prevAmount = toBigIntSafe(prevAsset.amount);
327+
const curAmount = toBigIntSafe(asset.amount);
328+
if (prevAmount == null || curAmount == null) {
329+
continue;
330+
}
331+
332+
// 이전에 0이였다가 밸런스가 생긴경우.
333+
if (prevAmount === BigInt(0) && curAmount > BigInt(0)) {
334+
return true;
335+
}
336+
337+
// 이전 밸런스에 배해서 10% 밸런스가 증가한 경우
338+
if (
339+
prevAmount > BigInt(0) &&
340+
curAmount * BigInt(10) >= prevAmount * BigInt(11)
341+
) {
342+
return true;
343+
}
344+
}
345+
}
346+
347+
return false;
135348
}
136349

137350
@computed
@@ -423,6 +636,24 @@ export class ChainStore extends BaseChainStore<ChainInfoWithCoreTypes> {
423636
});
424637
});
425638

639+
const dismissedNewTokenFound = yield* toGenerator(
640+
this.kvStore.get<Record<string, boolean>>("dismissedNewTokenFound")
641+
);
642+
643+
if (dismissedNewTokenFound) {
644+
for (const [key, value] of Object.entries(dismissedNewTokenFound)) {
645+
runInAction(() => {
646+
this._newTokenFoundDismissed.set(key, value);
647+
});
648+
}
649+
}
650+
651+
autorun(() => {
652+
const js = toJS(this._newTokenFoundDismissed);
653+
const obj = Object.fromEntries(js);
654+
this.kvStore.set<Record<string, boolean>>("dismissedNewTokenFound", obj);
655+
});
656+
426657
yield Promise.all([
427658
this.updateChainInfosFromBackground(),
428659
this.updateEnabledChainIdentifiersFromBackground(),
@@ -510,6 +741,8 @@ export class ChainStore extends BaseChainStore<ChainInfoWithCoreTypes> {
510741
this._tokenScans = yield* toGenerator(
511742
this.requester.sendMessage(BACKGROUND_PORT, new GetTokenScansMsg(id))
512743
);
744+
this.resetDismissIfNeeded(id, this._tokenScans);
745+
513746
(async () => {
514747
await new Promise<void>((resolve) => {
515748
const disposal = autorun(() => {
@@ -541,6 +774,7 @@ export class ChainStore extends BaseChainStore<ChainInfoWithCoreTypes> {
541774
runInAction(() => {
542775
this._tokenScans = res.tokenScans;
543776
});
777+
this.resetDismissIfNeeded(id, this._tokenScans);
544778
}
545779
}
546780
})();

0 commit comments

Comments
 (0)