88 toJS ,
99} from "mobx" ;
1010
11- import { ChainInfo , ModularChainInfo } from "@keplr-wallet/types" ;
11+ import { AppCurrency , ChainInfo , ModularChainInfo } from "@keplr-wallet/types" ;
1212import {
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" ;
3739import { BACKGROUND_PORT , MessageRequester } from "@keplr-wallet/router" ;
3840import { KVStore , toGenerator } from "@keplr-wallet/common" ;
3941import { ChainIdHelper } from "@keplr-wallet/cosmos" ;
4042
43+ type Assets = {
44+ currency : AppCurrency ;
45+ amount : string ;
46+ } ;
47+
4148export 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