@keplr-wallet/background 0.12.308 → 0.12.309-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,35 +2,56 @@ import { ChainsService } from "../chains";
2
2
  import { KeyRingCosmosService } from "../keyring-cosmos";
3
3
  import { KeyRingService } from "../keyring";
4
4
  import { ChainsUIForegroundService, ChainsUIService } from "../chains-ui";
5
- import { autorun, makeObservable, observable, runInAction, toJS } from "mobx";
5
+ import {
6
+ action,
7
+ autorun,
8
+ makeObservable,
9
+ observable,
10
+ runInAction,
11
+ toJS,
12
+ } from "mobx";
6
13
  import { AppCurrency, SupportedPaymentType } from "@keplr-wallet/types";
7
14
  import { simpleFetch } from "@keplr-wallet/simple-fetch";
8
15
  import { Dec } from "@keplr-wallet/unit";
9
16
  import { ChainIdHelper } from "@keplr-wallet/cosmos";
10
17
  import { VaultService } from "../vault";
11
- import { KVStore } from "@keplr-wallet/common";
18
+ import { DenomHelper, KVStore } from "@keplr-wallet/common";
12
19
  import { KeyRingStarknetService } from "../keyring-starknet";
13
20
  import { CairoUint256 } from "starknet";
14
21
  import { KeyRingBitcoinService } from "../keyring-bitcoin";
15
22
  import { MessageRequester } from "@keplr-wallet/router";
16
23
 
24
+ const thirdpartySupportedChainIdMap: Record<string, string> = {
25
+ "eip155:1": "eth",
26
+ "eip155:10": "opt",
27
+ "eip155:137": "polygon",
28
+ "eip155:8453": "base",
29
+ "eip155:42161": "arb",
30
+ };
31
+
32
+ type Asset = {
33
+ currency?: AppCurrency;
34
+ coinMinimalDenom?: string;
35
+ amount: string;
36
+ };
37
+
38
+ export type TokenScanInfo = {
39
+ bech32Address?: string;
40
+ ethereumHexAddress?: string;
41
+ starknetHexAddress?: string;
42
+ bitcoinAddress?: {
43
+ bech32Address: string;
44
+ paymentType: SupportedPaymentType;
45
+ };
46
+ coinType?: number;
47
+ assets: Asset[];
48
+ };
49
+
17
50
  export type TokenScan = {
18
51
  chainId: string;
19
- infos: {
20
- bech32Address?: string;
21
- ethereumHexAddress?: string;
22
- starknetHexAddress?: string;
23
- bitcoinAddress?: {
24
- bech32Address: string;
25
- paymentType: SupportedPaymentType;
26
- };
27
- coinType?: number;
28
- assets: {
29
- currency: AppCurrency;
30
- amount: string;
31
- }[];
32
- }[];
52
+ infos: TokenScanInfo[];
33
53
  linkedChainKey?: string;
54
+ dismissedInfos?: TokenScanInfo[];
34
55
  };
35
56
 
36
57
  export class TokenScanService {
@@ -127,31 +148,42 @@ export class TokenScanService {
127
148
  });
128
149
  }
129
150
  );
130
- this.chainsUIService.addChainUIEnabledChangedHandler(
131
- (vaultId, chainIdentifiers) => {
132
- runInAction(() => {
133
- let prevTokenScans = this.vaultToMap.get(vaultId);
134
- if (prevTokenScans) {
135
- prevTokenScans = prevTokenScans.filter((tokenScan) => {
136
- return !chainIdentifiers.includes(
137
- ChainIdHelper.parse(tokenScan.chainId).identifier
138
- );
139
- });
140
- this.vaultToMap.set(vaultId, prevTokenScans);
141
- }
142
- });
143
- }
144
- );
151
+
152
+ this.chainsService.addChainRemovedHandler((chainInfo) => {
153
+ runInAction(() => {
154
+ for (const [vaultId, tokenScans] of this.vaultToMap.entries()) {
155
+ let prevTokenScans = tokenScans;
156
+ prevTokenScans = prevTokenScans.filter((scan) => {
157
+ return scan.chainId !== chainInfo.chainId;
158
+ });
159
+
160
+ this.vaultToMap.set(vaultId, prevTokenScans);
161
+ }
162
+ });
163
+ });
145
164
  }
146
165
 
147
166
  getTokenScans(vaultId: string): TokenScan[] {
148
167
  return (this.vaultToMap.get(vaultId) ?? [])
149
168
  .filter((tokenScan) => {
150
169
  return (
151
- this.chainsService.hasChainInfo(tokenScan.chainId) ||
152
- this.chainsService.hasModularChainInfo(tokenScan.chainId)
170
+ (this.chainsService.hasChainInfo(tokenScan.chainId) ||
171
+ this.chainsService.hasModularChainInfo(tokenScan.chainId)) &&
172
+ !this.chainsUIService.isEnabled(vaultId, tokenScan.chainId)
153
173
  );
154
174
  })
175
+ .filter((tokenScan) => {
176
+ let hasAmount = false;
177
+ for (const info of tokenScan.infos) {
178
+ for (const asset of info.assets) {
179
+ if (asset.amount && asset.amount !== "0") {
180
+ hasAmount = true;
181
+ break;
182
+ }
183
+ }
184
+ }
185
+ return hasAmount;
186
+ })
155
187
  .sort((a, b) => {
156
188
  // Sort by chain name
157
189
  const aChainInfo = this.chainsService.hasChainInfo(a.chainId)
@@ -225,6 +257,13 @@ export class TokenScanService {
225
257
  const chainIdentifier = ChainIdHelper.parse(
226
258
  tokenScan.chainId
227
259
  ).identifier;
260
+
261
+ const prevTokenScan = prevTokenScans.find((scan) => {
262
+ return (
263
+ ChainIdHelper.parse(scan.chainId).identifier === chainIdentifier
264
+ );
265
+ });
266
+
228
267
  prevTokenScans = prevTokenScans.filter((scan) => {
229
268
  const prevChainIdentifier = ChainIdHelper.parse(
230
269
  scan.chainId
@@ -232,7 +271,10 @@ export class TokenScanService {
232
271
  return chainIdentifier !== prevChainIdentifier;
233
272
  });
234
273
 
235
- prevTokenScans.push(tokenScan);
274
+ prevTokenScans.push({
275
+ ...prevTokenScan,
276
+ ...tokenScan,
277
+ });
236
278
 
237
279
  this.vaultToMap.set(vaultId, prevTokenScans);
238
280
  });
@@ -254,6 +296,7 @@ export class TokenScanService {
254
296
  const tokenScans: TokenScan[] = [];
255
297
  const processedLinkedChainKeys = new Set<string>();
256
298
  const promises: Promise<void>[] = [];
299
+ const logChains: string[] = [];
257
300
 
258
301
  for (const modularChainInfo of modularChainInfos) {
259
302
  if ("linkedChainKey" in modularChainInfo) {
@@ -275,10 +318,18 @@ export class TokenScanService {
275
318
  }
276
319
  })()
277
320
  );
321
+ logChains.push(modularChainInfo.chainId);
278
322
  }
279
323
 
280
324
  // ignore error
281
- await Promise.allSettled(promises);
325
+ const settled = await Promise.allSettled(promises);
326
+ for (let i = 0; i < settled.length; i++) {
327
+ const s = settled[i];
328
+ if (s.status === "rejected") {
329
+ console.error("failed to calculateTokenScan", logChains[i]);
330
+ console.error(s.reason);
331
+ }
332
+ }
282
333
 
283
334
  if (tokenScans.length > 0) {
284
335
  runInAction(() => {
@@ -288,6 +339,13 @@ export class TokenScanService {
288
339
  const chainIdentifier = ChainIdHelper.parse(
289
340
  tokenScan.chainId
290
341
  ).identifier;
342
+
343
+ const prevTokenScan = prevTokenScans.find((scan) => {
344
+ return (
345
+ ChainIdHelper.parse(scan.chainId).identifier === chainIdentifier
346
+ );
347
+ });
348
+
291
349
  prevTokenScans = prevTokenScans.filter((scan) => {
292
350
  const prevChainIdentifier = ChainIdHelper.parse(
293
351
  scan.chainId
@@ -295,13 +353,12 @@ export class TokenScanService {
295
353
  return chainIdentifier !== prevChainIdentifier;
296
354
  });
297
355
 
298
- prevTokenScans.push(tokenScan);
356
+ prevTokenScans.push({
357
+ ...prevTokenScan,
358
+ ...tokenScan,
359
+ });
299
360
  }
300
361
 
301
- prevTokenScans = prevTokenScans.filter((scan) => {
302
- return !this.chainsUIService.isEnabled(vaultId, scan.chainId);
303
- });
304
-
305
362
  this.vaultToMap.set(vaultId, prevTokenScans);
306
363
  });
307
364
  }
@@ -346,6 +403,8 @@ export class TokenScanService {
346
403
  pubkey.getEthAddress()
347
404
  ).toString("hex")}`;
348
405
 
406
+ const assets: Asset[] = [];
407
+
349
408
  const res = await simpleFetch<{
350
409
  result: string;
351
410
  }>(evmInfo.rpc, {
@@ -373,16 +432,79 @@ export class TokenScanService {
373
432
  res.status === 200 &&
374
433
  BigInt(res.data.result).toString(10) !== "0"
375
434
  ) {
435
+ assets.push({
436
+ currency: chainInfo.stakeCurrency ?? chainInfo.currencies[0],
437
+ amount: BigInt(res.data.result).toString(10),
438
+ });
439
+ }
440
+
441
+ if (thirdpartySupportedChainIdMap[chainId]) {
442
+ const tokenAPIURL = `https://evm-${chainId.replace(
443
+ "eip155:",
444
+ ""
445
+ )}.keplr.app/api`;
446
+
447
+ const res = await simpleFetch<{
448
+ jsonrpc: string;
449
+ id: number;
450
+ result: {
451
+ address: string;
452
+ tokenBalances: {
453
+ contractAddress: string;
454
+ tokenBalance: string | null;
455
+ error: {
456
+ code: number;
457
+ message: string;
458
+ } | null;
459
+ }[];
460
+ // TODO: Support pagination.
461
+ pageKey: string;
462
+ };
463
+ }>(tokenAPIURL, {
464
+ method: "POST",
465
+ headers: {
466
+ "content-type": "application/json",
467
+ ...(() => {
468
+ if (typeof browser !== "undefined") {
469
+ return {
470
+ "request-source": new URL(browser.runtime.getURL("/"))
471
+ .origin,
472
+ };
473
+ }
474
+ return undefined;
475
+ })(),
476
+ },
477
+ body: JSON.stringify({
478
+ jsonrpc: "2.0",
479
+ method: "alchemy_getTokenBalances",
480
+ params: [ethereumHexAddress, "erc20"],
481
+ id: 1,
482
+ }),
483
+ });
484
+
485
+ if (res.status === 200) {
486
+ for (const tokenBalance of res.data.result?.tokenBalances ?? []) {
487
+ if (tokenBalance.tokenBalance && tokenBalance.error == null) {
488
+ const amount = BigInt(tokenBalance.tokenBalance).toString(10);
489
+ if (amount !== "0") {
490
+ assets.push({
491
+ coinMinimalDenom: DenomHelper.normalizeDenom(
492
+ `erc20:${tokenBalance.contractAddress}`
493
+ ),
494
+ amount,
495
+ });
496
+ }
497
+ }
498
+ }
499
+ }
500
+ }
501
+
502
+ if (assets.length > 0) {
376
503
  tokenScan.infos.push({
377
504
  bech32Address: "",
378
505
  ethereumHexAddress,
379
506
  coinType: 60,
380
- assets: [
381
- {
382
- currency: chainInfo.stakeCurrency ?? chainInfo.currencies[0],
383
- amount: BigInt(res.data.result).toString(10),
384
- },
385
- ],
507
+ assets,
386
508
  });
387
509
  }
388
510
  } else {
@@ -422,26 +544,26 @@ export class TokenScanService {
422
544
  );
423
545
 
424
546
  if (res.status === 200) {
425
- const assets: TokenScan["infos"][number]["assets"] = [];
547
+ const assets: TokenScanInfo["assets"] = [];
426
548
 
427
549
  const balances = res.data?.balances ?? [];
428
550
  for (const bal of balances) {
429
551
  const currency = chainInfo.currencies.find(
430
552
  (cur) => cur.coinMinimalDenom === bal.denom
431
553
  );
432
- if (currency) {
433
- // validate
434
- if (typeof bal.amount !== "string") {
435
- throw new Error("Invalid amount");
436
- }
437
554
 
438
- const dec = new Dec(bal.amount);
439
- if (dec.gt(new Dec(0))) {
440
- assets.push({
441
- currency,
442
- amount: bal.amount,
443
- });
444
- }
555
+ // validate
556
+ if (typeof bal.amount !== "string") {
557
+ throw new Error("Invalid amount");
558
+ }
559
+
560
+ const dec = new Dec(bal.amount);
561
+ if (dec.gt(new Dec(0))) {
562
+ assets.push({
563
+ currency,
564
+ coinMinimalDenom: bal.denom,
565
+ amount: bal.amount,
566
+ });
445
567
  }
446
568
  }
447
569
 
@@ -636,17 +758,128 @@ export class TokenScanService {
636
758
  chainId,
637
759
  false
638
760
  );
639
-
640
761
  if (bitcoinScanInfo) {
641
762
  tokenScan.infos.push(bitcoinScanInfo);
642
763
  }
643
764
  }
644
765
  }
645
-
646
766
  if (tokenScan.infos.length > 0) {
647
767
  return tokenScan;
648
768
  }
649
769
 
650
770
  return undefined;
651
771
  }
772
+
773
+ @action
774
+ dismissNewTokenFoundInHome(vaultId: string) {
775
+ const prevTokenScans = this.vaultToMap.get(vaultId) ?? [];
776
+ for (const prevTokenScan of prevTokenScans) {
777
+ prevTokenScan.dismissedInfos = prevTokenScan.infos;
778
+ }
779
+ this.vaultToMap.set(vaultId, prevTokenScans);
780
+ }
781
+
782
+ @action
783
+ resetDismiss(vaultId: string) {
784
+ const prevTokenScans = this.vaultToMap.get(vaultId) ?? [];
785
+ for (const prevTokenScan of prevTokenScans) {
786
+ prevTokenScan.dismissedInfos = undefined;
787
+ }
788
+ this.vaultToMap.set(vaultId, prevTokenScans);
789
+ }
790
+
791
+ public isMeaningfulTokenScanChangeBetweenDismissed(
792
+ tokenScan: TokenScan
793
+ ): boolean {
794
+ if (!tokenScan.dismissedInfos || tokenScan.dismissedInfos.length === 0) {
795
+ return tokenScan.infos.length > 0;
796
+ }
797
+
798
+ const makeKey = (info: TokenScanInfo): string | undefined => {
799
+ if (info.bech32Address) return `bech32:${info.bech32Address}`;
800
+ if (info.ethereumHexAddress) return `eth:${info.ethereumHexAddress}`;
801
+ if (info.starknetHexAddress) return `stark:${info.starknetHexAddress}`;
802
+ if (info.bitcoinAddress?.bech32Address)
803
+ return `btc:${info.bitcoinAddress.bech32Address}`;
804
+ if (info.coinType != null) return `coin:${info.coinType}`;
805
+ return undefined;
806
+ };
807
+
808
+ const toBigIntSafe = (v: string): bigint | undefined => {
809
+ try {
810
+ return BigInt(v);
811
+ } catch {
812
+ return undefined;
813
+ }
814
+ };
815
+
816
+ const dismissedTokenInfosMap = new Map<string, TokenScanInfo>();
817
+ for (const info of tokenScan.dismissedInfos ?? []) {
818
+ const key = makeKey(info);
819
+ if (key) {
820
+ dismissedTokenInfosMap.set(key, info);
821
+ }
822
+ }
823
+
824
+ for (const info of tokenScan.infos) {
825
+ const key = makeKey(info);
826
+ if (!key) {
827
+ continue;
828
+ }
829
+
830
+ const dismissedTokenInfo = dismissedTokenInfosMap.get(key);
831
+
832
+ if (!dismissedTokenInfo) {
833
+ if (info.assets.length > 0) {
834
+ return true;
835
+ }
836
+ continue;
837
+ }
838
+
839
+ const dismissedAssetMap = new Map<string, Asset>();
840
+ for (const asset of dismissedTokenInfo.assets) {
841
+ const coinMinimalDenom =
842
+ asset.currency?.coinMinimalDenom || asset.coinMinimalDenom;
843
+ if (!coinMinimalDenom) {
844
+ continue;
845
+ }
846
+ dismissedAssetMap.set(coinMinimalDenom, asset);
847
+ }
848
+
849
+ for (const asset of info.assets) {
850
+ const coinMinimalDenom =
851
+ asset.currency?.coinMinimalDenom || asset.coinMinimalDenom;
852
+ if (!coinMinimalDenom) {
853
+ continue;
854
+ }
855
+ const prevAsset = dismissedAssetMap.get(coinMinimalDenom);
856
+
857
+ // 없던 토큰이 생긴경우
858
+ if (!prevAsset) {
859
+ return true;
860
+ }
861
+
862
+ const prevAmount = toBigIntSafe(prevAsset.amount);
863
+ const curAmount = toBigIntSafe(asset.amount);
864
+ if (prevAmount == null || curAmount == null) {
865
+ continue;
866
+ }
867
+
868
+ // 이전에 0이였다가 밸런스가 생긴경우.
869
+ if (prevAmount === BigInt(0) && curAmount > BigInt(0)) {
870
+ return true;
871
+ }
872
+
873
+ // 이전 밸런스에 배해서 10% 밸런스가 증가한 경우
874
+ if (
875
+ prevAmount > BigInt(0) &&
876
+ curAmount * BigInt(10) >= prevAmount * BigInt(11)
877
+ ) {
878
+ return true;
879
+ }
880
+ }
881
+ }
882
+
883
+ return false;
884
+ }
652
885
  }
package/src/tx/service.ts CHANGED
@@ -251,7 +251,7 @@ export class BackgroundTxService {
251
251
  const starknet = modularChainInfo.starknet;
252
252
  const provider = new RpcProvider({
253
253
  nodeUrl: starknet.rpc,
254
- specVersion: "0.8.1",
254
+ specVersion: "0.9.0",
255
255
  });
256
256
 
257
257
  this.notification.create({