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

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,7 +2,11 @@ import { Message } from "@keplr-wallet/router";
2
2
  import { TokenScan } from "./service";
3
3
  import { ROUTE } from "./constants";
4
4
 
5
- export class GetTokenScansMsg extends Message<TokenScan[]> {
5
+ export class GetTokenScansMsg extends Message<{
6
+ vaultId: string;
7
+ tokenScans: TokenScan[];
8
+ tokenScansWithoutDismissed: TokenScan[];
9
+ }> {
6
10
  public static type() {
7
11
  return "get-token-scans";
8
12
  }
@@ -29,6 +33,7 @@ export class GetTokenScansMsg extends Message<TokenScan[]> {
29
33
  export class RevalidateTokenScansMsg extends Message<{
30
34
  vaultId: string;
31
35
  tokenScans: TokenScan[];
36
+ tokenScansWithoutDismissed: TokenScan[];
32
37
  }> {
33
38
  public static type() {
34
39
  return "revalidate-token-scans";
@@ -52,3 +57,31 @@ export class RevalidateTokenScansMsg extends Message<{
52
57
  return RevalidateTokenScansMsg.type();
53
58
  }
54
59
  }
60
+
61
+ export class DismissNewTokenFoundInMainMsg extends Message<{
62
+ vaultId: string;
63
+ tokenScans: TokenScan[];
64
+ tokenScansWithoutDismissed: TokenScan[];
65
+ }> {
66
+ public static type() {
67
+ return "dismiss-new-token-found-in-main";
68
+ }
69
+
70
+ constructor(public readonly vaultId: string) {
71
+ super();
72
+ }
73
+
74
+ validateBasic(): void {
75
+ if (!this.vaultId) {
76
+ throw new Error("Empty vault id");
77
+ }
78
+ }
79
+
80
+ route(): string {
81
+ return ROUTE;
82
+ }
83
+
84
+ type(): string {
85
+ return DismissNewTokenFoundInMainMsg.type();
86
+ }
87
+ }
@@ -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,29 +148,28 @@ 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
  })
155
175
  .sort((a, b) => {
@@ -225,6 +245,13 @@ export class TokenScanService {
225
245
  const chainIdentifier = ChainIdHelper.parse(
226
246
  tokenScan.chainId
227
247
  ).identifier;
248
+
249
+ const prevTokenScan = prevTokenScans.find((scan) => {
250
+ return (
251
+ ChainIdHelper.parse(scan.chainId).identifier === chainIdentifier
252
+ );
253
+ });
254
+
228
255
  prevTokenScans = prevTokenScans.filter((scan) => {
229
256
  const prevChainIdentifier = ChainIdHelper.parse(
230
257
  scan.chainId
@@ -232,7 +259,10 @@ export class TokenScanService {
232
259
  return chainIdentifier !== prevChainIdentifier;
233
260
  });
234
261
 
235
- prevTokenScans.push(tokenScan);
262
+ prevTokenScans.push({
263
+ ...prevTokenScan,
264
+ ...tokenScan,
265
+ });
236
266
 
237
267
  this.vaultToMap.set(vaultId, prevTokenScans);
238
268
  });
@@ -254,6 +284,7 @@ export class TokenScanService {
254
284
  const tokenScans: TokenScan[] = [];
255
285
  const processedLinkedChainKeys = new Set<string>();
256
286
  const promises: Promise<void>[] = [];
287
+ const logChains: string[] = [];
257
288
 
258
289
  for (const modularChainInfo of modularChainInfos) {
259
290
  if ("linkedChainKey" in modularChainInfo) {
@@ -275,10 +306,18 @@ export class TokenScanService {
275
306
  }
276
307
  })()
277
308
  );
309
+ logChains.push(modularChainInfo.chainId);
278
310
  }
279
311
 
280
312
  // ignore error
281
- await Promise.allSettled(promises);
313
+ const settled = await Promise.allSettled(promises);
314
+ for (let i = 0; i < settled.length; i++) {
315
+ const s = settled[i];
316
+ if (s.status === "rejected") {
317
+ console.error("failed to calculateTokenScan", logChains[i]);
318
+ console.error(s.reason);
319
+ }
320
+ }
282
321
 
283
322
  if (tokenScans.length > 0) {
284
323
  runInAction(() => {
@@ -288,6 +327,13 @@ export class TokenScanService {
288
327
  const chainIdentifier = ChainIdHelper.parse(
289
328
  tokenScan.chainId
290
329
  ).identifier;
330
+
331
+ const prevTokenScan = prevTokenScans.find((scan) => {
332
+ return (
333
+ ChainIdHelper.parse(scan.chainId).identifier === chainIdentifier
334
+ );
335
+ });
336
+
291
337
  prevTokenScans = prevTokenScans.filter((scan) => {
292
338
  const prevChainIdentifier = ChainIdHelper.parse(
293
339
  scan.chainId
@@ -295,13 +341,12 @@ export class TokenScanService {
295
341
  return chainIdentifier !== prevChainIdentifier;
296
342
  });
297
343
 
298
- prevTokenScans.push(tokenScan);
344
+ prevTokenScans.push({
345
+ ...prevTokenScan,
346
+ ...tokenScan,
347
+ });
299
348
  }
300
349
 
301
- prevTokenScans = prevTokenScans.filter((scan) => {
302
- return !this.chainsUIService.isEnabled(vaultId, scan.chainId);
303
- });
304
-
305
350
  this.vaultToMap.set(vaultId, prevTokenScans);
306
351
  });
307
352
  }
@@ -346,6 +391,8 @@ export class TokenScanService {
346
391
  pubkey.getEthAddress()
347
392
  ).toString("hex")}`;
348
393
 
394
+ const assets: Asset[] = [];
395
+
349
396
  const res = await simpleFetch<{
350
397
  result: string;
351
398
  }>(evmInfo.rpc, {
@@ -373,16 +420,76 @@ export class TokenScanService {
373
420
  res.status === 200 &&
374
421
  BigInt(res.data.result).toString(10) !== "0"
375
422
  ) {
423
+ assets.push({
424
+ currency: chainInfo.stakeCurrency ?? chainInfo.currencies[0],
425
+ amount: BigInt(res.data.result).toString(10),
426
+ });
427
+ }
428
+
429
+ if (thirdpartySupportedChainIdMap[chainId]) {
430
+ const tokenAPIURL = `https://evm-${chainId.replace(
431
+ "eip155:",
432
+ ""
433
+ )}.keplr.app/api`;
434
+
435
+ const res = await simpleFetch<{
436
+ jsonrpc: string;
437
+ id: number;
438
+ result: {
439
+ address: string;
440
+ tokenBalances: {
441
+ contractAddress: string;
442
+ tokenBalance: string | null;
443
+ error: {
444
+ code: number;
445
+ message: string;
446
+ } | null;
447
+ }[];
448
+ // TODO: Support pagination.
449
+ pageKey: string;
450
+ };
451
+ }>(tokenAPIURL, {
452
+ method: "POST",
453
+ headers: {
454
+ "content-type": "application/json",
455
+ ...(() => {
456
+ if (typeof browser !== "undefined") {
457
+ return {
458
+ "request-source": new URL(browser.runtime.getURL("/"))
459
+ .origin,
460
+ };
461
+ }
462
+ return undefined;
463
+ })(),
464
+ },
465
+ body: JSON.stringify({
466
+ jsonrpc: "2.0",
467
+ method: "alchemy_getTokenBalances",
468
+ params: [ethereumHexAddress, "erc20"],
469
+ id: 1,
470
+ }),
471
+ });
472
+
473
+ if (res.status === 200) {
474
+ for (const tokenBalance of res.data.result?.tokenBalances ?? []) {
475
+ if (tokenBalance.tokenBalance && tokenBalance.error == null) {
476
+ assets.push({
477
+ coinMinimalDenom: DenomHelper.normalizeDenom(
478
+ `erc20:${tokenBalance.contractAddress}`
479
+ ),
480
+ amount: BigInt(tokenBalance.tokenBalance).toString(10),
481
+ });
482
+ }
483
+ }
484
+ }
485
+ }
486
+
487
+ if (assets.length > 0) {
376
488
  tokenScan.infos.push({
377
489
  bech32Address: "",
378
490
  ethereumHexAddress,
379
491
  coinType: 60,
380
- assets: [
381
- {
382
- currency: chainInfo.stakeCurrency ?? chainInfo.currencies[0],
383
- amount: BigInt(res.data.result).toString(10),
384
- },
385
- ],
492
+ assets,
386
493
  });
387
494
  }
388
495
  } else {
@@ -422,26 +529,26 @@ export class TokenScanService {
422
529
  );
423
530
 
424
531
  if (res.status === 200) {
425
- const assets: TokenScan["infos"][number]["assets"] = [];
532
+ const assets: TokenScanInfo["assets"] = [];
426
533
 
427
534
  const balances = res.data?.balances ?? [];
428
535
  for (const bal of balances) {
429
536
  const currency = chainInfo.currencies.find(
430
537
  (cur) => cur.coinMinimalDenom === bal.denom
431
538
  );
432
- if (currency) {
433
- // validate
434
- if (typeof bal.amount !== "string") {
435
- throw new Error("Invalid amount");
436
- }
437
539
 
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
- }
540
+ // validate
541
+ if (typeof bal.amount !== "string") {
542
+ throw new Error("Invalid amount");
543
+ }
544
+
545
+ const dec = new Dec(bal.amount);
546
+ if (dec.gte(new Dec(0))) {
547
+ assets.push({
548
+ currency,
549
+ coinMinimalDenom: bal.denom,
550
+ amount: bal.amount,
551
+ });
445
552
  }
446
553
  }
447
554
 
@@ -502,28 +609,26 @@ export class TokenScanService {
502
609
  .toBigInt()
503
610
  .toString(10);
504
611
 
505
- if (amount !== "0") {
506
- // XXX: Starknet의 경우는 여러 주소가 나올수가 없으므로
507
- // starknetHexAddress는 같은 값으로 나온다고 생각하고 처리한다.
508
- if (tokenScan.infos.length === 0) {
509
- tokenScan.infos.push({
510
- starknetHexAddress,
511
- assets: [
512
- {
513
- currency,
514
- amount,
515
- },
516
- ],
517
- });
518
- } else {
519
- if (
520
- tokenScan.infos[0].starknetHexAddress === starknetHexAddress
521
- ) {
522
- tokenScan.infos[0].assets.push({
612
+ // XXX: Starknet의 경우는 여러 주소가 나올수가 없으므로
613
+ // starknetHexAddress는 같은 값으로 나온다고 생각하고 처리한다.
614
+ if (tokenScan.infos.length === 0) {
615
+ tokenScan.infos.push({
616
+ starknetHexAddress,
617
+ assets: [
618
+ {
523
619
  currency,
524
620
  amount,
525
- });
526
- }
621
+ },
622
+ ],
623
+ });
624
+ } else {
625
+ if (
626
+ tokenScan.infos[0].starknetHexAddress === starknetHexAddress
627
+ ) {
628
+ tokenScan.infos[0].assets.push({
629
+ currency,
630
+ amount,
631
+ });
527
632
  }
528
633
  }
529
634
  }
@@ -613,40 +718,135 @@ export class TokenScanService {
613
718
  );
614
719
  }
615
720
 
616
- let hasNonZeroAmount = false;
617
-
618
- for (const bitcoinScanInfo of bitcoinScanInfos) {
619
- // 우선 main currency만 처리한다.
620
- if (
621
- bitcoinScanInfo.assets.length > 0 &&
622
- bitcoinScanInfo.assets[0].amount !== "0"
623
- ) {
624
- hasNonZeroAmount = true;
625
- break;
626
- }
627
- }
628
-
629
- // 하나라도 0이 아닌 값이 있으면 연결된 모든 체인에 대해 토큰 스캔 정보를 추가한다.
630
- if (hasNonZeroAmount) {
631
- tokenScan.infos.push(...bitcoinScanInfos);
632
- }
721
+ tokenScan.infos.push(...bitcoinScanInfos);
633
722
  } else {
634
723
  const bitcoinScanInfo = await getBitcoinScanInfo(
635
724
  vaultId,
636
725
  chainId,
637
- false
726
+ true
638
727
  );
639
-
640
728
  if (bitcoinScanInfo) {
641
729
  tokenScan.infos.push(bitcoinScanInfo);
642
730
  }
643
731
  }
644
732
  }
645
-
646
733
  if (tokenScan.infos.length > 0) {
647
734
  return tokenScan;
648
735
  }
649
736
 
650
737
  return undefined;
651
738
  }
739
+
740
+ @action
741
+ dismissNewTokenFoundInHome(vaultId: string) {
742
+ const prevTokenScans = this.vaultToMap.get(vaultId) ?? [];
743
+ for (const prevTokenScan of prevTokenScans) {
744
+ prevTokenScan.dismissedInfos = prevTokenScan.infos;
745
+ }
746
+ this.vaultToMap.set(vaultId, prevTokenScans);
747
+ }
748
+
749
+ @action
750
+ resetDismiss(vaultId: string) {
751
+ const prevTokenScans = this.vaultToMap.get(vaultId) ?? [];
752
+ for (const prevTokenScan of prevTokenScans) {
753
+ prevTokenScan.dismissedInfos = undefined;
754
+ }
755
+ this.vaultToMap.set(vaultId, prevTokenScans);
756
+ }
757
+
758
+ public isMeaningfulTokenScanChangeBetweenDismissed(
759
+ tokenScan: TokenScan
760
+ ): boolean {
761
+ if (!tokenScan.dismissedInfos || tokenScan.dismissedInfos.length === 0) {
762
+ return tokenScan.infos.length > 0;
763
+ }
764
+
765
+ const makeKey = (info: TokenScanInfo): string | undefined => {
766
+ if (info.bech32Address) return `bech32:${info.bech32Address}`;
767
+ if (info.ethereumHexAddress) return `eth:${info.ethereumHexAddress}`;
768
+ if (info.starknetHexAddress) return `stark:${info.starknetHexAddress}`;
769
+ if (info.bitcoinAddress?.bech32Address)
770
+ return `btc:${info.bitcoinAddress.bech32Address}`;
771
+ if (info.coinType != null) return `coin:${info.coinType}`;
772
+ return undefined;
773
+ };
774
+
775
+ const toBigIntSafe = (v: string): bigint | undefined => {
776
+ try {
777
+ return BigInt(v);
778
+ } catch {
779
+ return undefined;
780
+ }
781
+ };
782
+
783
+ const dismissedTokenInfosMap = new Map<string, TokenScanInfo>();
784
+ for (const info of tokenScan.dismissedInfos ?? []) {
785
+ const key = makeKey(info);
786
+ if (key) {
787
+ dismissedTokenInfosMap.set(key, info);
788
+ }
789
+ }
790
+
791
+ for (const info of tokenScan.infos) {
792
+ const key = makeKey(info);
793
+ if (!key) {
794
+ continue;
795
+ }
796
+
797
+ const dismissedTokenInfo = dismissedTokenInfosMap.get(key);
798
+
799
+ if (!dismissedTokenInfo) {
800
+ if (info.assets.length > 0) {
801
+ return true;
802
+ }
803
+ continue;
804
+ }
805
+
806
+ const dismissedAssetMap = new Map<string, Asset>();
807
+ for (const asset of dismissedTokenInfo.assets) {
808
+ const coinMinimalDenom =
809
+ asset.currency?.coinMinimalDenom || asset.coinMinimalDenom;
810
+ if (!coinMinimalDenom) {
811
+ continue;
812
+ }
813
+ dismissedAssetMap.set(coinMinimalDenom, asset);
814
+ }
815
+
816
+ for (const asset of info.assets) {
817
+ const coinMinimalDenom =
818
+ asset.currency?.coinMinimalDenom || asset.coinMinimalDenom;
819
+ if (!coinMinimalDenom) {
820
+ continue;
821
+ }
822
+ const prevAsset = dismissedAssetMap.get(coinMinimalDenom);
823
+
824
+ // 없던 토큰이 생긴경우
825
+ if (!prevAsset) {
826
+ return true;
827
+ }
828
+
829
+ const prevAmount = toBigIntSafe(prevAsset.amount);
830
+ const curAmount = toBigIntSafe(asset.amount);
831
+ if (prevAmount == null || curAmount == null) {
832
+ continue;
833
+ }
834
+
835
+ // 이전에 0이였다가 밸런스가 생긴경우.
836
+ if (prevAmount === BigInt(0) && curAmount > BigInt(0)) {
837
+ return true;
838
+ }
839
+
840
+ // 이전 밸런스에 배해서 10% 밸런스가 증가한 경우
841
+ if (
842
+ prevAmount > BigInt(0) &&
843
+ curAmount * BigInt(10) >= prevAmount * BigInt(11)
844
+ ) {
845
+ return true;
846
+ }
847
+ }
848
+ }
849
+
850
+ return false;
851
+ }
652
852
  }
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({