@keplr-wallet/background 0.12.313 → 0.13.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.
Files changed (83) hide show
  1. package/build/index.d.ts +1 -0
  2. package/build/index.js +7 -1
  3. package/build/index.js.map +1 -1
  4. package/build/keyring-cosmos/service.d.ts +10 -0
  5. package/build/keyring-cosmos/service.js +100 -0
  6. package/build/keyring-cosmos/service.js.map +1 -1
  7. package/build/keyring-ethereum/service.d.ts +5 -0
  8. package/build/keyring-ethereum/service.js +66 -0
  9. package/build/keyring-ethereum/service.js.map +1 -1
  10. package/build/recent-send-history/api.d.ts +31 -0
  11. package/build/recent-send-history/api.js +97 -0
  12. package/build/recent-send-history/api.js.map +1 -0
  13. package/build/recent-send-history/handler.js +36 -0
  14. package/build/recent-send-history/handler.js.map +1 -1
  15. package/build/recent-send-history/init.js +5 -0
  16. package/build/recent-send-history/init.js.map +1 -1
  17. package/build/recent-send-history/messages.d.ts +76 -1
  18. package/build/recent-send-history/messages.js +121 -1
  19. package/build/recent-send-history/messages.js.map +1 -1
  20. package/build/recent-send-history/service.d.ts +262 -9
  21. package/build/recent-send-history/service.js +2103 -812
  22. package/build/recent-send-history/service.js.map +1 -1
  23. package/build/recent-send-history/types.d.ts +214 -22
  24. package/build/recent-send-history/types.js +21 -0
  25. package/build/recent-send-history/types.js.map +1 -1
  26. package/build/tx/service.d.ts +2 -0
  27. package/build/tx/service.js +35 -0
  28. package/build/tx/service.js.map +1 -1
  29. package/build/tx-ethereum/service.d.ts +2 -0
  30. package/build/tx-ethereum/service.js +42 -0
  31. package/build/tx-ethereum/service.js.map +1 -1
  32. package/build/tx-executor/constants.d.ts +1 -0
  33. package/build/tx-executor/constants.js +5 -0
  34. package/build/tx-executor/constants.js.map +1 -0
  35. package/build/tx-executor/handler.d.ts +3 -0
  36. package/build/tx-executor/handler.js +45 -0
  37. package/build/tx-executor/handler.js.map +1 -0
  38. package/build/tx-executor/index.d.ts +3 -0
  39. package/build/tx-executor/index.js +20 -0
  40. package/build/tx-executor/index.js.map +1 -0
  41. package/build/tx-executor/init.d.ts +3 -0
  42. package/build/tx-executor/init.js +14 -0
  43. package/build/tx-executor/init.js.map +1 -0
  44. package/build/tx-executor/internal.d.ts +4 -0
  45. package/build/tx-executor/internal.js +24 -0
  46. package/build/tx-executor/internal.js.map +1 -0
  47. package/build/tx-executor/messages.d.ts +53 -0
  48. package/build/tx-executor/messages.js +116 -0
  49. package/build/tx-executor/messages.js.map +1 -0
  50. package/build/tx-executor/service.d.ts +67 -0
  51. package/build/tx-executor/service.js +715 -0
  52. package/build/tx-executor/service.js.map +1 -0
  53. package/build/tx-executor/types.d.ts +105 -0
  54. package/build/tx-executor/types.js +33 -0
  55. package/build/tx-executor/types.js.map +1 -0
  56. package/build/tx-executor/utils/cosmos.d.ts +59 -0
  57. package/build/tx-executor/utils/cosmos.js +526 -0
  58. package/build/tx-executor/utils/cosmos.js.map +1 -0
  59. package/build/tx-executor/utils/evm.d.ts +4 -0
  60. package/build/tx-executor/utils/evm.js +236 -0
  61. package/build/tx-executor/utils/evm.js.map +1 -0
  62. package/package.json +13 -13
  63. package/src/index.ts +24 -1
  64. package/src/keyring-cosmos/service.ts +151 -0
  65. package/src/keyring-ethereum/service.ts +103 -6
  66. package/src/recent-send-history/api.ts +119 -0
  67. package/src/recent-send-history/handler.ts +84 -0
  68. package/src/recent-send-history/init.ts +10 -0
  69. package/src/recent-send-history/messages.ts +163 -1
  70. package/src/recent-send-history/service.ts +3042 -1153
  71. package/src/recent-send-history/types.ts +268 -31
  72. package/src/tx/service.ts +41 -0
  73. package/src/tx-ethereum/service.ts +57 -0
  74. package/src/tx-executor/constants.ts +1 -0
  75. package/src/tx-executor/handler.ts +71 -0
  76. package/src/tx-executor/index.ts +3 -0
  77. package/src/tx-executor/init.ts +20 -0
  78. package/src/tx-executor/internal.ts +9 -0
  79. package/src/tx-executor/messages.ts +157 -0
  80. package/src/tx-executor/service.ts +1025 -0
  81. package/src/tx-executor/types.ts +161 -0
  82. package/src/tx-executor/utils/cosmos.ts +771 -0
  83. package/src/tx-executor/utils/evm.ts +310 -0
@@ -1,4 +1,5 @@
1
1
  import { ChainsService } from "../chains";
2
+ import { AnalyticsService } from "../analytics";
2
3
  import {
3
4
  Bech32Address,
4
5
  ChainIdHelper,
@@ -17,10 +18,15 @@ import {
17
18
  import { KVStore, retry } from "@keplr-wallet/common";
18
19
  import {
19
20
  IBCHistory,
21
+ IBCSwapMinimalTrackingData,
22
+ IbcHop,
20
23
  RecentSendHistory,
21
24
  SkipHistory,
22
- StatusRequest,
23
- TxStatusResponse,
25
+ SwapProvider,
26
+ SwapV2History,
27
+ SwapV2RouteStepStatus,
28
+ SwapV2TxStatus,
29
+ SwapV2TxStatusResponse,
24
30
  } from "./types";
25
31
  import { Buffer } from "buffer/";
26
32
  import {
@@ -30,8 +36,18 @@ import {
30
36
  EthTxStatus,
31
37
  } from "@keplr-wallet/types";
32
38
  import { CoinPretty } from "@keplr-wallet/unit";
33
- import { simpleFetch } from "@keplr-wallet/simple-fetch";
34
39
  import { id } from "@ethersproject/hash";
40
+ import { EventBusPublisher } from "@keplr-wallet/common";
41
+ import { TxExecutionEvent } from "../tx-executor/types";
42
+ import {
43
+ requestSkipTxTrack,
44
+ requestSwapV2TxStatus,
45
+ requestEthTxReceipt,
46
+ requestSkipTxStatus,
47
+ requestEthTxTrace,
48
+ } from "./api";
49
+
50
+ export const UNKNOWN_TX_STATUS_TIMEOUT_MS = 5 * 60 * 1000; // 5분
35
51
 
36
52
  const SWAP_API_ENDPOINT = process.env["KEPLR_API_ENDPOINT"] ?? "";
37
53
 
@@ -53,16 +69,38 @@ export class RecentSendHistoryService {
53
69
  @observable
54
70
  protected readonly recentSkipHistoryMap: Map<string, SkipHistory> = new Map();
55
71
 
72
+ @observable
73
+ protected recentSwapV2HistorySeq: number = 0;
74
+ // Key: id (sequence, it should be increased by 1 for each)
75
+ @observable
76
+ protected readonly recentSwapV2HistoryMap: Map<string, SwapV2History> =
77
+ new Map();
78
+
56
79
  constructor(
57
80
  protected readonly kvStore: KVStore,
58
81
  protected readonly chainsService: ChainsService,
59
82
  protected readonly txService: BackgroundTxService,
60
- protected readonly notification: Notification
83
+ protected readonly analyticsService: AnalyticsService,
84
+ protected readonly notification: Notification,
85
+ protected readonly publisher: EventBusPublisher<TxExecutionEvent>
61
86
  ) {
62
87
  makeObservable(this);
63
88
  }
64
89
 
90
+ // ============================================================================
91
+ // Init – load & persist histories
92
+ // ============================================================================
93
+
65
94
  async init(): Promise<void> {
95
+ await this.initRecentSendHistory();
96
+ await this.initRecentIBCHistory();
97
+ await this.initRecentSkipHistory();
98
+ await this.initRecentSwapV2History();
99
+
100
+ this.chainsService.addChainRemovedHandler(this.onChainRemoved);
101
+ }
102
+
103
+ protected async initRecentSendHistory(): Promise<void> {
66
104
  const recentSendHistoryMapSaved = await this.kvStore.get<
67
105
  Record<string, RecentSendHistory[]>
68
106
  >("recentSendHistoryMap");
@@ -81,7 +119,9 @@ export class RecentSendHistoryService {
81
119
  obj
82
120
  );
83
121
  });
122
+ }
84
123
 
124
+ protected async initRecentIBCHistory(): Promise<void> {
85
125
  // 밑의 storage의 key들이 ibc transfer를 포함하는데
86
126
  // 이 이유는 이전에 transfer history만 지원되었을때
87
127
  // key를 그렇게 정했었기 때문이다
@@ -127,9 +167,18 @@ export class RecentSendHistoryService {
127
167
  });
128
168
 
129
169
  for (const history of this.getRecentIBCHistories()) {
130
- this.trackIBCPacketForwardingRecursive(history.id);
170
+ this.trackIBCPacketForwardingRecursive((onFulfill, onClose, onError) => {
171
+ this.trackIBCPacketForwardingRecursiveInternal(
172
+ history.id,
173
+ onFulfill,
174
+ onClose,
175
+ onError
176
+ );
177
+ });
131
178
  }
179
+ }
132
180
 
181
+ protected async initRecentSkipHistory(): Promise<void> {
133
182
  // Load skip history sequence from the storage
134
183
  const recentSkipHistorySeqSaved = await this.kvStore.get<number>(
135
184
  "recentSkipHistorySeq"
@@ -178,10 +227,63 @@ export class RecentSendHistoryService {
178
227
  for (const history of this.getRecentSkipHistories()) {
179
228
  this.trackSkipSwapRecursive(history.id);
180
229
  }
230
+ }
181
231
 
182
- this.chainsService.addChainRemovedHandler(this.onChainRemoved);
232
+ protected async initRecentSwapV2History(): Promise<void> {
233
+ const recentSwapV2HistorySeqSaved = await this.kvStore.get<number>(
234
+ "recentSwapV2HistorySeq"
235
+ );
236
+ if (recentSwapV2HistorySeqSaved) {
237
+ runInAction(() => {
238
+ this.recentSwapV2HistorySeq = recentSwapV2HistorySeqSaved;
239
+ });
240
+ }
241
+
242
+ // Save the swap v2 history sequence to the storage when the swap v2 history sequence is changed
243
+ autorun(() => {
244
+ const js = toJS(this.recentSwapV2HistorySeq);
245
+ this.kvStore.set<number>("recentSwapV2HistorySeq", js);
246
+ });
247
+
248
+ // Load swap v2 history from the storage
249
+ const recentSwapV2HistoryMapSaved = await this.kvStore.get<
250
+ Record<string, SwapV2History>
251
+ >("recentSwapV2HistoryMap");
252
+ if (recentSwapV2HistoryMapSaved) {
253
+ runInAction(() => {
254
+ let entries = Object.entries(recentSwapV2HistoryMapSaved);
255
+ entries = entries.sort(([, a], [, b]) => {
256
+ return parseInt(a.id) - parseInt(b.id);
257
+ });
258
+ for (const [key, value] of entries) {
259
+ this.recentSwapV2HistoryMap.set(key, value);
260
+ }
261
+ });
262
+ }
263
+
264
+ // Save the swap v2 history to the storage when the swap v2 history is changed
265
+ autorun(() => {
266
+ const js = toJS(this.recentSwapV2HistoryMap);
267
+ const obj = Object.fromEntries(js);
268
+ this.kvStore.set<Record<string, SwapV2History>>(
269
+ "recentSwapV2HistoryMap",
270
+ obj
271
+ );
272
+ });
273
+
274
+ for (const history of this.getRecentSwapV2Histories()) {
275
+ this.trackSwapV2Recursive(history.id);
276
+
277
+ if (history.additionalTrackingData && !history.additionalTrackDone) {
278
+ this.trackSwapV2AdditionalRecursive(history.id);
279
+ }
280
+ }
183
281
  }
184
282
 
283
+ // ============================================================================
284
+ // Send tx and record
285
+ // ============================================================================
286
+
185
287
  async sendTxAndRecord(
186
288
  type: string,
187
289
  sourceChainId: string,
@@ -244,23 +346,10 @@ export class RecentSendHistoryService {
244
346
  if (shouldLegacyTrack) {
245
347
  // no wait
246
348
  setTimeout(() => {
247
- simpleFetch<any>(SWAP_API_ENDPOINT, "/v1/swap/tx", {
248
- method: "POST",
249
- headers: {
250
- "content-type": "application/json",
251
- ...(() => {
252
- const res: { authorization?: string } = {};
253
- if (process.env["SKIP_API_KEY"]) {
254
- res.authorization = process.env["SKIP_API_KEY"];
255
- }
256
-
257
- return res;
258
- })(),
259
- },
260
- body: JSON.stringify({
261
- tx_hash: Buffer.from(tx.hash).toString("hex"),
262
- chain_id: sourceChainId,
263
- }),
349
+ requestSkipTxTrack({
350
+ endpoint: SWAP_API_ENDPOINT,
351
+ chainId: sourceChainId,
352
+ txHash: Buffer.from(tx.hash).toString("hex"),
264
353
  })
265
354
  .then((result) => {
266
355
  console.log(
@@ -290,12 +379,46 @@ export class RecentSendHistoryService {
290
379
  txHash
291
380
  );
292
381
 
293
- this.trackIBCPacketForwardingRecursive(id);
382
+ this.trackIBCPacketForwardingRecursive((onFulfill, onClose, onError) => {
383
+ this.trackIBCPacketForwardingRecursiveInternal(
384
+ id,
385
+ onFulfill,
386
+ onClose,
387
+ onError
388
+ );
389
+ });
294
390
  }
295
391
 
296
392
  return txHash;
297
393
  }
298
394
 
395
+ getRecentSendHistories(chainId: string, type: string): RecentSendHistory[] {
396
+ const key = `${ChainIdHelper.parse(chainId).identifier}/${type}`;
397
+ return (this.recentSendHistoryMap.get(key) ?? []).slice(0, 20);
398
+ }
399
+
400
+ @action
401
+ addRecentSendHistory(
402
+ chainId: string,
403
+ type: string,
404
+ history: Omit<RecentSendHistory, "timestamp">
405
+ ) {
406
+ const key = `${ChainIdHelper.parse(chainId).identifier}/${type}`;
407
+
408
+ let histories = this.recentSendHistoryMap.get(key) ?? [];
409
+ histories.unshift({
410
+ timestamp: Date.now(),
411
+ ...history,
412
+ });
413
+ histories = histories.slice(0, 20);
414
+
415
+ this.recentSendHistoryMap.set(key, histories);
416
+ }
417
+
418
+ // ============================================================================
419
+ // Send tx and record IBC swap/transfer
420
+ // ============================================================================
421
+
299
422
  async sendTxAndRecordIBCSwap(
300
423
  swapType: "amount-in" | "amount-out",
301
424
  sourceChainId: string,
@@ -343,23 +466,10 @@ export class RecentSendHistoryService {
343
466
  if (shouldLegacyTrack) {
344
467
  setTimeout(() => {
345
468
  // no wait
346
- simpleFetch<any>(SWAP_API_ENDPOINT, "/v1/swap/tx", {
347
- method: "POST",
348
- headers: {
349
- "content-type": "application/json",
350
- ...(() => {
351
- const res: { authorization?: string } = {};
352
- if (process.env["SKIP_API_KEY"]) {
353
- res.authorization = process.env["SKIP_API_KEY"];
354
- }
355
-
356
- return res;
357
- })(),
358
- },
359
- body: JSON.stringify({
360
- tx_hash: Buffer.from(tx.hash).toString("hex"),
361
- chain_id: sourceChainId,
362
- }),
469
+ requestSkipTxTrack({
470
+ endpoint: SWAP_API_ENDPOINT,
471
+ chainId: sourceChainId,
472
+ txHash: Buffer.from(tx.hash).toString("hex"),
363
473
  })
364
474
  .then((result) => {
365
475
  console.log(
@@ -392,18 +502,184 @@ export class RecentSendHistoryService {
392
502
  txHash
393
503
  );
394
504
 
395
- this.trackIBCPacketForwardingRecursive(id);
505
+ this.trackIBCPacketForwardingRecursive((onFulfill, onClose, onError) => {
506
+ this.trackIBCPacketForwardingRecursiveInternal(
507
+ id,
508
+ onFulfill,
509
+ onClose,
510
+ onError
511
+ );
512
+ });
396
513
  }
397
514
 
398
515
  return txHash;
399
516
  }
400
517
 
401
- trackIBCPacketForwardingRecursive(id: string): void {
518
+ @action
519
+ addRecentIBCTransferHistory(
520
+ chainId: string,
521
+ destinationChainId: string,
522
+ sender: string,
523
+ recipient: string,
524
+ amount: {
525
+ amount: string;
526
+ denom: string;
527
+ }[],
528
+ memo: string,
529
+ ibcChannels:
530
+ | {
531
+ portId: string;
532
+ channelId: string;
533
+ counterpartyChainId: string;
534
+ }[],
535
+ notificationInfo: {
536
+ currencies: AppCurrency[];
537
+ },
538
+ txHash: Uint8Array
539
+ ): string {
540
+ const id = (this.recentIBCHistorySeq++).toString();
541
+
542
+ const history: IBCHistory = {
543
+ id,
544
+ chainId,
545
+ destinationChainId,
546
+ timestamp: Date.now(),
547
+ sender,
548
+ recipient,
549
+ amount,
550
+ memo,
551
+
552
+ ibcHistory: ibcChannels.map((channel) => {
553
+ return {
554
+ portId: channel.portId,
555
+ channelId: channel.channelId,
556
+ counterpartyChainId: channel.counterpartyChainId,
557
+
558
+ completed: false,
559
+ };
560
+ }),
561
+ notificationInfo,
562
+ txHash: Buffer.from(txHash).toString("hex"),
563
+ };
564
+
565
+ this.recentIBCHistoryMap.set(id, history);
566
+
567
+ return id;
568
+ }
569
+
570
+ @action
571
+ addRecentIBCSwapHistory(
572
+ swapType: "amount-in" | "amount-out",
573
+ chainId: string,
574
+ destinationChainId: string,
575
+ sender: string,
576
+ amount: {
577
+ amount: string;
578
+ denom: string;
579
+ }[],
580
+ memo: string,
581
+ ibcChannels:
582
+ | {
583
+ portId: string;
584
+ channelId: string;
585
+ counterpartyChainId: string;
586
+ }[],
587
+ destinationAsset: {
588
+ chainId: string;
589
+ denom: string;
590
+ },
591
+ swapChannelIndex: number,
592
+ swapReceiver: string[],
593
+ notificationInfo: {
594
+ currencies: AppCurrency[];
595
+ },
596
+ txHash: Uint8Array
597
+ ): string {
598
+ const id = (this.recentIBCHistorySeq++).toString();
599
+
600
+ const history: IBCHistory = {
601
+ id,
602
+ swapType,
603
+ chainId,
604
+ destinationChainId,
605
+ timestamp: Date.now(),
606
+ sender,
607
+ amount,
608
+ memo,
609
+
610
+ ibcHistory: ibcChannels.map((channel) => {
611
+ return {
612
+ portId: channel.portId,
613
+ channelId: channel.channelId,
614
+ counterpartyChainId: channel.counterpartyChainId,
615
+
616
+ completed: false,
617
+ };
618
+ }),
619
+ destinationAsset,
620
+ swapChannelIndex,
621
+ swapReceiver,
622
+ resAmount: [],
623
+ notificationInfo,
624
+ txHash: Buffer.from(txHash).toString("hex"),
625
+ };
626
+
627
+ this.recentIBCHistoryMap.set(id, history);
628
+
629
+ return id;
630
+ }
631
+
632
+ getRecentIBCHistory(id: string): IBCHistory | undefined {
633
+ return this.recentIBCHistoryMap.get(id);
634
+ }
635
+
636
+ getRecentIBCHistories(): IBCHistory[] {
637
+ return Array.from(this.recentIBCHistoryMap.values()).filter((history) => {
638
+ if (!this.chainsService.hasChainInfo(history.chainId)) {
639
+ return false;
640
+ }
641
+
642
+ if (!this.chainsService.hasChainInfo(history.destinationChainId)) {
643
+ return false;
644
+ }
645
+
646
+ if (
647
+ history.ibcHistory.some((history) => {
648
+ return !this.chainsService.hasChainInfo(history.counterpartyChainId);
649
+ })
650
+ ) {
651
+ return false;
652
+ }
653
+
654
+ return true;
655
+ });
656
+ }
657
+
658
+ @action
659
+ removeRecentIBCHistory(id: string): boolean {
660
+ return this.recentIBCHistoryMap.delete(id);
661
+ }
662
+
663
+ @action
664
+ clearAllRecentIBCHistory(): void {
665
+ this.recentIBCHistoryMap.clear();
666
+ }
667
+
668
+ // ============================================================================
669
+ // Common functions for history tracking (IBC, Skip, Swap V2)
670
+ // ============================================================================
671
+
672
+ trackIBCPacketForwardingRecursive(
673
+ trackHandler: (
674
+ onFulfill: () => void,
675
+ onClose: () => void,
676
+ onError: () => void
677
+ ) => void
678
+ ): void {
402
679
  retry(
403
680
  () => {
404
681
  return new Promise<void>((resolve, reject) => {
405
- this.trackIBCPacketForwardingRecursiveInternal(
406
- id,
682
+ trackHandler(
407
683
  () => {
408
684
  resolve();
409
685
  },
@@ -449,718 +725,624 @@ export class RecentSendHistoryService {
449
725
  return;
450
726
  }
451
727
 
452
- const needRewind = (() => {
453
- if (!history.txFulfilled) {
454
- return false;
455
- }
456
-
457
- if (history.ibcHistory.length === 0) {
458
- return false;
459
- }
460
-
461
- return history.ibcHistory.find((h) => h.error != null) != null;
462
- })();
463
-
464
- if (needRewind) {
465
- if (history.ibcHistory.find((h) => h.rewoundButNextRewindingBlocked)) {
466
- onFulfill();
467
- return;
468
- }
469
- const isTimeoutPacket = history.packetTimeout || false;
470
- const lastRewoundChannelIndex = history.ibcHistory.findIndex((h) => {
471
- if (h.rewound) {
472
- return true;
473
- }
728
+ if (!history.txFulfilled) {
729
+ this.trackIBCTxFulfillment({
730
+ chainId: history.chainId,
731
+ txHash: history.txHash,
732
+ ibcHistory: history.ibcHistory,
733
+ swapReceiver:
734
+ "swapReceiver" in history ? history.swapReceiver : undefined,
735
+ onTxFulfilled: (_tx, firstHopResAmount) => {
736
+ runInAction(() => {
737
+ history.txFulfilled = true;
738
+ if ("swapReceiver" in history && firstHopResAmount) {
739
+ history.resAmount.push(firstHopResAmount);
740
+ }
741
+ });
742
+ this.trackIBCPacketForwardingRecursive(
743
+ (onFulfill, onClose, onError) => {
744
+ this.trackIBCPacketForwardingRecursiveInternal(
745
+ id,
746
+ onFulfill,
747
+ onClose,
748
+ onError
749
+ );
750
+ }
751
+ );
752
+ },
753
+ onTxError: () => {
754
+ this.removeRecentIBCHistory(id);
755
+ },
756
+ onFulfill: onFulfill,
757
+ onClose: onClose,
758
+ onError: onError,
474
759
  });
475
- const targetChannel = (() => {
476
- if (lastRewoundChannelIndex >= 0) {
477
- if (lastRewoundChannelIndex === 0) {
478
- return undefined;
479
- }
760
+ return;
761
+ }
480
762
 
481
- return history.ibcHistory[lastRewoundChannelIndex - 1];
482
- }
483
- return history.ibcHistory.find((h) => h.error != null);
484
- })();
485
- const isSwapTargetChannel =
486
- targetChannel &&
487
- "swapChannelIndex" in history &&
488
- history.ibcHistory.indexOf(targetChannel) ===
489
- history.swapChannelIndex + 1;
490
-
491
- if (targetChannel && targetChannel.sequence) {
492
- const prevChainInfo = (() => {
493
- const targetChannelIndex = history.ibcHistory.findIndex(
494
- (h) => h === targetChannel
495
- );
496
- if (targetChannelIndex < 0) {
497
- return undefined;
498
- }
499
- if (targetChannelIndex === 0) {
500
- return this.chainsService.getChainInfo(history.chainId);
501
- }
502
- return this.chainsService.getChainInfo(
503
- history.ibcHistory[targetChannelIndex - 1].counterpartyChainId
504
- );
505
- })();
506
- if (prevChainInfo) {
507
- const txTracer = new TendermintTxTracer(
508
- prevChainInfo.rpc,
509
- "/websocket"
763
+ if (
764
+ this.handleIbcRewindIfNeeded({
765
+ sourceChainId: history.chainId,
766
+ ibcHistory: history.ibcHistory,
767
+ packetTimeout: history.packetTimeout,
768
+ swapContext:
769
+ "swapReceiver" in history && "swapChannelIndex" in history
770
+ ? {
771
+ swapReceiver: history.swapReceiver,
772
+ swapChannelIndex: history.swapChannelIndex,
773
+ setSwapRefundInfo: (refundInfo) => {
774
+ runInAction(() => {
775
+ history.swapRefundInfo = refundInfo;
776
+ });
777
+ },
778
+ }
779
+ : undefined,
780
+ onFulfill,
781
+ onClose,
782
+ onError,
783
+ onRewindComplete: () => {
784
+ this.trackIBCPacketForwardingRecursive(
785
+ (onFulfill, onClose, onError) => {
786
+ this.trackIBCPacketForwardingRecursiveInternal(
787
+ id,
788
+ onFulfill,
789
+ onClose,
790
+ onError
791
+ );
792
+ }
510
793
  );
511
- txTracer.addEventListener("close", onClose);
512
- txTracer.addEventListener("error", onError);
513
- txTracer
514
- .traceTx(
515
- isTimeoutPacket
516
- ? {
517
- // "timeout_packet.packet_src_port": targetChannel.portId,
518
- "timeout_packet.packet_src_channel":
519
- targetChannel.channelId,
520
- "timeout_packet.packet_sequence": targetChannel.sequence,
521
- }
522
- : {
523
- // "acknowledge_packet.packet_src_port": targetChannel.portId,
524
- "acknowledge_packet.packet_src_channel":
525
- targetChannel.channelId,
526
- "acknowledge_packet.packet_sequence":
527
- targetChannel.sequence,
528
- }
529
- )
530
- .then((res: any) => {
531
- txTracer.close();
794
+ },
795
+ })
796
+ ) {
797
+ return;
798
+ }
532
799
 
533
- if (!res) {
534
- return;
535
- }
800
+ this.trackIbcHopFlowWithTimeout({
801
+ ibcHistory: history.ibcHistory,
802
+ sourceChainId: history.chainId,
803
+ swapReceiver:
804
+ "swapReceiver" in history ? history.swapReceiver : undefined,
805
+ onHopCompleted: (resAmount) => {
806
+ runInAction(() => {
807
+ if (resAmount && "resAmount" in history) {
808
+ history.resAmount.push(resAmount);
809
+ }
810
+ });
811
+ },
812
+ onAllCompleted: () => {
813
+ const notificationInfo = history.notificationInfo;
814
+ if (notificationInfo && !history.notified) {
815
+ runInAction(() => {
816
+ history.notified = true;
817
+ });
536
818
 
537
- runInAction(() => {
538
- if (isSwapTargetChannel) {
539
- const txs = res.txs
540
- ? res.txs.map((res: any) => res.tx_result || res)
541
- : [res.tx_result || res];
542
- if (txs && Array.isArray(txs)) {
543
- for (const tx of txs) {
544
- if (targetChannel.sequence && "swapReceiver" in history) {
545
- const index = isTimeoutPacket
546
- ? this.getIBCTimeoutPacketIndexFromTx(
547
- tx,
548
- targetChannel.portId,
549
- targetChannel.channelId,
550
- targetChannel.sequence
551
- )
552
- : this.getIBCAcknowledgementPacketIndexFromTx(
553
- tx,
554
- targetChannel.portId,
555
- targetChannel.channelId,
556
- targetChannel.sequence
557
- );
558
- if (index >= 0) {
559
- // 좀 빡치게 timeout packet은 refund 로직이 실행되고 나서 "timeout_packet" event가 발생한다.
560
- const refunded = isTimeoutPacket
561
- ? this.getIBCSwapResAmountFromTx(
562
- tx,
563
- history.swapReceiver[
564
- history.swapChannelIndex + 1
565
- ],
566
- (() => {
567
- const i =
568
- this.getLastIBCTimeoutPacketBeforeIndexFromTx(
569
- tx,
570
- index
571
- );
572
-
573
- if (i < 0) {
574
- return 0;
575
- }
576
- return i;
577
- })(),
578
- index
579
- )
580
- : this.getIBCSwapResAmountFromTx(
581
- tx,
582
- history.swapReceiver[
583
- history.swapChannelIndex + 1
584
- ],
585
- index
586
- );
587
- history.swapRefundInfo = {
588
- chainId: prevChainInfo.chainId,
589
- amount: refunded,
590
- };
591
-
592
- targetChannel.rewoundButNextRewindingBlocked = true;
593
- break;
594
- }
595
- }
819
+ const chainInfo = this.chainsService.getChainInfo(
820
+ history.destinationChainId
821
+ );
822
+ if (chainInfo) {
823
+ if ("swapType" in history) {
824
+ if (history.resAmount.length > 0) {
825
+ const amount = history.resAmount[history.resAmount.length - 1];
826
+ const assetsText = amount
827
+ .map((amt) => {
828
+ const currency = notificationInfo.currencies.find(
829
+ (cur) => cur.coinMinimalDenom === amt.denom
830
+ );
831
+ if (!currency) {
832
+ return undefined;
596
833
  }
597
- }
834
+ return new CoinPretty(currency, amt.amount)
835
+ .hideIBCMetadata(true)
836
+ .shrink(true)
837
+ .maxDecimals(6)
838
+ .inequalitySymbol(true)
839
+ .trim(true)
840
+ .toString();
841
+ })
842
+ .filter((text): text is string => Boolean(text));
843
+ if (assetsText.length > 0) {
844
+ this.notification.create({
845
+ iconRelativeUrl: "assets/logo-256.png",
846
+ title: "IBC Swap Succeeded",
847
+ message: `${assetsText.join(", ")} received on ${
848
+ chainInfo.chainName
849
+ }`,
850
+ });
598
851
  }
599
- targetChannel.rewound = true;
600
- });
601
- onFulfill();
602
- this.trackIBCPacketForwardingRecursive(id);
603
- });
852
+ }
853
+ } else {
854
+ const assetsText = history.amount
855
+ .map((amt) => {
856
+ const currency = notificationInfo.currencies.find(
857
+ (cur) => cur.coinMinimalDenom === amt.denom
858
+ );
859
+ if (!currency) {
860
+ return undefined;
861
+ }
862
+ return new CoinPretty(currency, amt.amount)
863
+ .hideIBCMetadata(true)
864
+ .shrink(true)
865
+ .maxDecimals(6)
866
+ .inequalitySymbol(true)
867
+ .trim(true)
868
+ .toString();
869
+ })
870
+ .filter((text): text is string => Boolean(text));
871
+ if (assetsText.length > 0) {
872
+ this.notification.create({
873
+ iconRelativeUrl: "assets/logo-256.png",
874
+ title: "IBC Transfer Succeeded",
875
+ message: `${assetsText.join(", ")} sent to ${
876
+ chainInfo.chainName
877
+ }`,
878
+ });
879
+ }
880
+ }
881
+ }
604
882
  }
605
- }
606
- } else if (!history.txFulfilled) {
607
- const chainId = history.chainId;
608
- const chainInfo = this.chainsService.getChainInfo(chainId);
609
- const txHash = Buffer.from(history.txHash, "hex");
883
+ },
884
+ onContinue: () => {
885
+ this.trackIBCPacketForwardingRecursive(
886
+ (onFulfill, onClose, onError) => {
887
+ this.trackIBCPacketForwardingRecursiveInternal(
888
+ id,
889
+ onFulfill,
890
+ onClose,
891
+ onError
892
+ );
893
+ }
894
+ );
895
+ },
896
+ onRetry: () => {
897
+ this.trackIBCPacketForwardingRecursive(
898
+ (onFulfill, onClose, onError) => {
899
+ this.trackIBCPacketForwardingRecursiveInternal(
900
+ id,
901
+ onFulfill,
902
+ onClose,
903
+ onError
904
+ );
905
+ }
906
+ );
907
+ },
908
+ onPacketTimeout: () => {
909
+ runInAction(() => {
910
+ history.packetTimeout = true;
911
+ });
912
+ },
913
+ onFulfill,
914
+ onClose,
915
+ onError,
916
+ });
917
+ };
610
918
 
611
- if (chainInfo) {
612
- const txTracer = new TendermintTxTracer(chainInfo.rpc, "/websocket");
613
- txTracer.addEventListener("close", onClose);
614
- txTracer.addEventListener("error", onError);
919
+ protected checkAndTrackSwapTxFulfilledRecursive = (params: {
920
+ chainId: string;
921
+ txHash: string;
922
+ onSuccess: () => void;
923
+ onPending: () => void;
924
+ onFailed: () => void;
925
+ onError: () => void;
926
+ }): void => {
927
+ const { chainId, txHash, onSuccess, onPending, onFailed, onError } = params;
928
+ const chainInfo = this.chainsService.getChainInfo(chainId);
929
+ if (!chainInfo) {
930
+ onFailed();
931
+ return;
932
+ }
615
933
 
616
- txTracer.traceTx(txHash).then((tx) => {
617
- txTracer.close();
934
+ this.resolveTxExecutionStatus(chainInfo, chainId, txHash)
935
+ .then((status) => {
936
+ switch (status) {
937
+ case "success":
938
+ onSuccess();
939
+ break;
940
+ case "pending":
941
+ onPending();
942
+ break;
943
+ case "failed":
944
+ onFailed();
945
+ break;
946
+ default:
947
+ onError();
948
+ break;
949
+ }
950
+ })
951
+ .catch(() => {
952
+ onError();
953
+ });
954
+ };
618
955
 
619
- runInAction(() => {
620
- history.txFulfilled = true;
621
- if (tx.code != null && tx.code !== 0) {
622
- history.txError = tx.log || tx.raw_log || "Unknown error";
956
+ protected async resolveTxExecutionStatus(
957
+ chainInfo: ChainInfo,
958
+ chainId: string,
959
+ txHash: string
960
+ ): Promise<"success" | "failed" | "pending" | "error"> {
961
+ if (this.chainsService.isEvmChain(chainId)) {
962
+ const evmInfo = chainInfo.evm;
963
+ if (!evmInfo) {
964
+ return Promise.resolve("error");
965
+ }
623
966
 
624
- // TODO: In this case, it is not currently displayed in the UI. So, delete it for now.
625
- // 어차피 tx 자체의 실패는 notification으로 알 수 있기 때문에 여기서 지우더라도 유저는 실패를 인지할 수 있다.
626
- this.removeRecentIBCHistory(id);
627
- } else {
628
- if ("swapReceiver" in history) {
629
- const resAmount = this.getIBCSwapResAmountFromTx(
630
- tx,
631
- history.swapReceiver[0]
632
- );
967
+ const res = await requestEthTxReceipt({
968
+ rpc: evmInfo.rpc,
969
+ txHash,
970
+ origin,
971
+ });
633
972
 
634
- history.resAmount.push(resAmount);
635
- }
973
+ if (res.data.error) {
974
+ return "error";
975
+ }
636
976
 
637
- if (history.ibcHistory.length > 0) {
638
- const firstChannel = history.ibcHistory[0];
639
-
640
- firstChannel.sequence = this.getIBCPacketSequenceFromTx(
641
- tx,
642
- firstChannel.portId,
643
- firstChannel.channelId
644
- );
645
- firstChannel.dstChannelId = this.getDstChannelIdFromTx(
646
- tx,
647
- firstChannel.portId,
648
- firstChannel.channelId
649
- );
650
-
651
- onFulfill();
652
- this.trackIBCPacketForwardingRecursive(id);
653
- }
654
- }
655
- });
656
- });
977
+ const txReceipt = res.data.result;
978
+ if (!txReceipt) {
979
+ return "pending";
657
980
  }
658
- } else if (history.ibcHistory.length > 0) {
659
- const targetChannelIndex = history.ibcHistory.findIndex((history) => {
660
- return !history.completed;
981
+ if (txReceipt.status === EthTxStatus.Success) {
982
+ return "success";
983
+ }
984
+
985
+ return "failed";
986
+ }
987
+
988
+ const txTracer = new TendermintTxTracer(chainInfo.rpc, "/websocket");
989
+ txTracer.addEventListener("error", () => {
990
+ txTracer.close();
991
+ });
992
+
993
+ return txTracer
994
+ .traceTx(Buffer.from(txHash.replace("0x", ""), "hex"))
995
+ .then((res: any) => {
996
+ txTracer.close();
997
+
998
+ const txResult = Array.isArray(res.txs)
999
+ ? res.txs && res.txs.length > 0
1000
+ ? res.txs[0].tx_result
1001
+ : undefined
1002
+ : res;
1003
+
1004
+ if (!txResult) {
1005
+ return "pending";
1006
+ }
1007
+ if (typeof txResult.code !== "number") {
1008
+ return "error";
1009
+ }
1010
+ return txResult.code === 0 ? "success" : "failed";
1011
+ })
1012
+ .catch(() => {
1013
+ txTracer.close();
1014
+ return "error";
661
1015
  });
662
- const targetChannel =
663
- targetChannelIndex >= 0
664
- ? history.ibcHistory[targetChannelIndex]
665
- : undefined;
666
- const nextChannel =
667
- targetChannelIndex >= 0 &&
668
- targetChannelIndex + 1 < history.ibcHistory.length
669
- ? history.ibcHistory[targetChannelIndex + 1]
670
- : undefined;
671
-
672
- if (targetChannel && targetChannel.sequence) {
673
- const closables: {
674
- readyState: WsReadyState;
675
- close: () => void;
676
- }[] = [];
677
- let _onFulfillOnce = false;
678
- const onFulfillOnce = () => {
679
- if (!_onFulfillOnce) {
680
- _onFulfillOnce = true;
681
- closables.forEach((closable) => {
682
- if (
683
- closable.readyState === WsReadyState.OPEN ||
684
- closable.readyState === WsReadyState.CONNECTING
685
- ) {
686
- closable.close();
687
- }
688
- });
689
- onFulfill();
690
- }
691
- };
692
- let _onCloseOnce = false;
693
- const onCloseOnce = () => {
694
- if (!_onCloseOnce) {
695
- _onCloseOnce = true;
696
- closables.forEach((closable) => {
697
- if (
698
- closable.readyState === WsReadyState.OPEN ||
699
- closable.readyState === WsReadyState.CONNECTING
700
- ) {
701
- closable.close();
702
- }
703
- });
704
- onClose();
1016
+ }
1017
+
1018
+ protected trackDestinationAssetAmount(params: {
1019
+ chainId: string;
1020
+ txHash: string;
1021
+ recipient: string;
1022
+ targetDenom: string;
1023
+ onResult: (resAmount: { amount: string; denom: string }[]) => void;
1024
+ onRefund?: (
1025
+ refundInfo: {
1026
+ chainId: string;
1027
+ amount: { amount: string; denom: string }[];
1028
+ },
1029
+ error?: string
1030
+ ) => void;
1031
+ onFulfill: () => void;
1032
+ }) {
1033
+ const {
1034
+ chainId,
1035
+ txHash,
1036
+ recipient,
1037
+ targetDenom,
1038
+ onResult,
1039
+ onRefund,
1040
+ onFulfill,
1041
+ } = params;
1042
+
1043
+ const chainInfo = this.chainsService.getChainInfo(chainId);
1044
+ if (!chainInfo) {
1045
+ onFulfill();
1046
+ return;
1047
+ }
1048
+
1049
+ if (this.chainsService.isEvmChain(chainId)) {
1050
+ this.traceEVMTransactionResult({
1051
+ chainId,
1052
+ txHash,
1053
+ recipient,
1054
+ targetDenom,
1055
+ onResult: (result) => {
1056
+ if (result.resAmount) {
1057
+ onResult(result.resAmount);
705
1058
  }
706
- };
707
- let _onErrorOnce = false;
708
- const onErrorOnce = () => {
709
- if (!_onErrorOnce) {
710
- _onErrorOnce = true;
711
- closables.forEach((closable) => {
712
- if (
713
- closable.readyState === WsReadyState.OPEN ||
714
- closable.readyState === WsReadyState.CONNECTING
715
- ) {
716
- closable.close();
717
- }
718
- });
719
- onError();
1059
+ if (result.refundInfo && onRefund) {
1060
+ onRefund(result.refundInfo, result.error);
720
1061
  }
721
- };
1062
+ },
1063
+ onFulfill,
1064
+ });
1065
+ return;
1066
+ }
722
1067
 
723
- const chainInfo = this.chainsService.getChainInfo(
724
- targetChannel.counterpartyChainId
725
- );
726
- if (chainInfo) {
727
- const queryEvents: any = {
728
- // "recv_packet.packet_src_port": targetChannel.portId,
729
- "recv_packet.packet_dst_channel": targetChannel.dstChannelId,
730
- "recv_packet.packet_sequence": targetChannel.sequence,
731
- };
1068
+ this.traceCosmosTransactionResult({
1069
+ chainInfo,
1070
+ txHash,
1071
+ recipient,
1072
+ onResult,
1073
+ onFulfill,
1074
+ });
1075
+ }
732
1076
 
733
- const txTracer = new TendermintTxTracer(chainInfo.rpc, "/websocket");
734
- closables.push(txTracer);
735
- txTracer.addEventListener("close", onCloseOnce);
736
- txTracer.addEventListener("error", onErrorOnce);
737
- txTracer.traceTx(queryEvents).then((res) => {
738
- txTracer.close();
1077
+ protected traceCosmosTransactionResult(params: {
1078
+ chainInfo: ChainInfo;
1079
+ txHash: string;
1080
+ recipient: string;
1081
+ onResult: (resAmount: { amount: string; denom: string }[]) => void;
1082
+ onFulfill: () => void;
1083
+ }) {
1084
+ const { chainInfo, txHash, recipient, onResult, onFulfill } = params;
1085
+ const txTracer = new TendermintTxTracer(chainInfo.rpc, "/websocket");
1086
+ txTracer.addEventListener("error", () => onFulfill());
1087
+ txTracer
1088
+ .queryTx({
1089
+ "tx.hash": txHash,
1090
+ })
1091
+ .then((res: any) => {
1092
+ txTracer.close();
739
1093
 
740
- if (!res) {
741
- return;
742
- }
1094
+ if (!res) {
1095
+ return;
1096
+ }
1097
+ const txs = res.txs
1098
+ ? res.txs.map((r: any) => r.tx_result || r)
1099
+ : [res.tx_result || res];
1100
+ for (const tx of txs) {
1101
+ const resAmount = this.getIBCSwapResAmountFromTx(tx, recipient);
1102
+ onResult(resAmount);
1103
+ return;
1104
+ }
1105
+ })
1106
+ .finally(() => {
1107
+ onFulfill();
1108
+ });
1109
+ }
743
1110
 
744
- const txs = res.txs
745
- ? res.txs.map((res: any) => res.tx_result || res)
746
- : [res.tx_result || res];
747
- if (txs && Array.isArray(txs)) {
748
- runInAction(() => {
749
- targetChannel.completed = true;
750
-
751
- for (const tx of txs) {
752
- try {
753
- const ack = this.getIBCWriteAcknowledgementAckFromTx(
754
- tx,
755
- targetChannel.portId,
756
- targetChannel.channelId,
757
- targetChannel.sequence!
758
- );
1111
+ // CHECK: move tracing logic (requestEthTxReceipt, requestEthTxTrace, parseEVMTxReceiptLogs) to tx-ethereum service
1112
+ protected traceEVMTransactionResult(params: {
1113
+ chainId: string;
1114
+ txHash: string;
1115
+ recipient: string;
1116
+ targetDenom: string;
1117
+ onResult: (result: {
1118
+ success: boolean;
1119
+ resAmount?: { amount: string; denom: string }[];
1120
+ refundInfo?: {
1121
+ chainId: string;
1122
+ amount: { amount: string; denom: string }[];
1123
+ };
1124
+ error?: string;
1125
+ }) => void;
1126
+ onFulfill: () => void;
1127
+ }): void {
1128
+ const { chainId, txHash, recipient, targetDenom, onResult, onFulfill } =
1129
+ params;
1130
+
1131
+ const chainInfo = this.chainsService.getChainInfo(chainId);
1132
+ if (!chainInfo) {
1133
+ onResult({ success: false });
1134
+ onFulfill();
1135
+ return;
1136
+ }
759
1137
 
760
- if (ack && ack.length > 0) {
761
- const str = Buffer.from(ack);
762
- try {
763
- const decoded = JSON.parse(str.toString());
764
- if (decoded.error) {
765
- // XXX: {key: 'packet_ack', value: '{"error":"ABCI code: 6: error handling packet: see events for details"}'}
766
- // 오류가 있을 경우 이딴식으로 오류가 나오기 때문에 뭐 유저에게 보여줄 방법이 없다...
767
- targetChannel.error = "Packet processing failed";
768
- onFulfillOnce();
769
- this.trackIBCPacketForwardingRecursive(id);
770
- break;
771
- }
772
- } catch (e) {
773
- // decode가 실패한 경우 사실 방법이 없다.
774
- // 일단 packet이 성공했다고 치고 진행한다.
775
- console.log(e);
776
- }
777
- }
1138
+ if (!this.chainsService.isEvmChain(chainId)) {
1139
+ onResult({ success: false, error: "Not an EVM chain" });
1140
+ onFulfill();
1141
+ return;
1142
+ }
778
1143
 
779
- // Because a tx can contain multiple messages, it's hard to know exactly which event we want.
780
- // But logically, the events closest to the recv_packet event is the events we want.
781
- const index = this.getIBCRecvPacketIndexFromTx(
782
- tx,
783
- targetChannel.portId,
784
- targetChannel.channelId,
785
- targetChannel.sequence!
786
- );
1144
+ const evmInfo = chainInfo.evm;
1145
+ if (!evmInfo) {
1146
+ onResult({ success: false });
1147
+ onFulfill();
1148
+ return;
1149
+ }
787
1150
 
788
- if (index >= 0) {
789
- if ("swapReceiver" in history) {
790
- const res: {
791
- amount: string;
792
- denom: string;
793
- }[] = this.getIBCSwapResAmountFromTx(
794
- tx,
795
- history.swapReceiver[targetChannelIndex + 1],
796
- index
797
- );
1151
+ requestEthTxReceipt({
1152
+ rpc: evmInfo.rpc,
1153
+ txHash,
1154
+ origin,
1155
+ })
1156
+ .then((res) => {
1157
+ const txReceipt = res.data.result;
1158
+ if (!txReceipt) {
1159
+ onResult({ success: false });
1160
+ return;
1161
+ }
798
1162
 
799
- history.resAmount.push(res);
800
- }
1163
+ requestEthTxTrace({
1164
+ rpc: evmInfo.rpc,
1165
+ txHash,
1166
+ origin,
1167
+ }).then((traceRes) => {
1168
+ let isFoundFromCall = false;
1169
+ const foundResAmount: { amount: string; denom: string }[] = [];
1170
+
1171
+ if (traceRes.data.result) {
1172
+ const searchForTransfers = (calls: any) => {
1173
+ for (const call of calls) {
1174
+ if (
1175
+ call.type === "CALL" &&
1176
+ call.to?.toLowerCase() === recipient.toLowerCase()
1177
+ ) {
1178
+ const isERC20Transfer = call.input?.startsWith("0xa9059cbb");
1179
+ const value = BigInt(
1180
+ isERC20Transfer
1181
+ ? `0x${call.input.substring(74)}`
1182
+ : call.value || "0x0"
1183
+ );
801
1184
 
802
- if (nextChannel) {
803
- nextChannel.sequence = this.getIBCPacketSequenceFromTx(
804
- tx,
805
- nextChannel.portId,
806
- nextChannel.channelId,
807
- index
808
- );
809
- nextChannel.dstChannelId = this.getDstChannelIdFromTx(
810
- tx,
811
- nextChannel.portId,
812
- nextChannel.channelId,
813
- index
814
- );
815
- onFulfillOnce();
816
- this.trackIBCPacketForwardingRecursive(id);
817
- break;
818
- } else {
819
- // Packet received to destination chain.
820
- if (history.notificationInfo && !history.notified) {
821
- runInAction(() => {
822
- history.notified = true;
823
- });
824
-
825
- const chainInfo = this.chainsService.getChainInfo(
826
- history.destinationChainId
827
- );
828
- if (chainInfo) {
829
- if ("swapType" in history) {
830
- if (history.resAmount.length > 0) {
831
- const amount =
832
- history.resAmount[
833
- history.resAmount.length - 1
834
- ];
835
- const assetsText = amount
836
- .filter((amt) =>
837
- history.notificationInfo!.currencies.find(
838
- (cur) =>
839
- cur.coinMinimalDenom === amt.denom
840
- )
841
- )
842
- .map((amt) => {
843
- const currency =
844
- history.notificationInfo!.currencies.find(
845
- (cur) =>
846
- cur.coinMinimalDenom === amt.denom
847
- );
848
- return new CoinPretty(currency!, amt.amount)
849
- .hideIBCMetadata(true)
850
- .shrink(true)
851
- .maxDecimals(6)
852
- .inequalitySymbol(true)
853
- .trim(true)
854
- .toString();
855
- });
856
- if (assetsText.length > 0) {
857
- // Notify user
858
- this.notification.create({
859
- iconRelativeUrl: "assets/logo-256.png",
860
- title: "IBC Swap Succeeded",
861
- message: `${assetsText.join(
862
- ", "
863
- )} received on ${chainInfo.chainName}`,
864
- });
865
- }
866
- }
867
- } else {
868
- const assetsText = history.amount
869
- .filter((amt) =>
870
- history.notificationInfo!.currencies.find(
871
- (cur) => cur.coinMinimalDenom === amt.denom
872
- )
873
- )
874
- .map((amt) => {
875
- const currency =
876
- history.notificationInfo!.currencies.find(
877
- (cur) =>
878
- cur.coinMinimalDenom === amt.denom
879
- );
880
- return new CoinPretty(currency!, amt.amount)
881
- .hideIBCMetadata(true)
882
- .shrink(true)
883
- .maxDecimals(6)
884
- .inequalitySymbol(true)
885
- .trim(true)
886
- .toString();
887
- });
888
- if (assetsText.length > 0) {
889
- // Notify user
890
- this.notification.create({
891
- iconRelativeUrl: "assets/logo-256.png",
892
- title: "IBC Transfer Succeeded",
893
- message: `${assetsText.join(", ")} sent to ${
894
- chainInfo.chainName
895
- }`,
896
- });
897
- }
898
- }
899
- }
900
- }
901
- onFulfillOnce();
902
- break;
903
- }
904
- }
905
- } catch {
906
- // noop
907
- }
1185
+ foundResAmount.push({
1186
+ amount: value.toString(10),
1187
+ denom: targetDenom,
1188
+ });
1189
+ isFoundFromCall = true;
908
1190
  }
909
- });
910
- }
911
- });
912
- }
913
1191
 
914
- let prevChainId: string = "";
915
- if (targetChannelIndex > 0) {
916
- prevChainId =
917
- history.ibcHistory[targetChannelIndex - 1].counterpartyChainId;
918
- } else {
919
- prevChainId = history.chainId;
920
- }
921
- if (prevChainId) {
922
- const prevChainInfo = this.chainsService.getChainInfo(prevChainId);
923
- if (prevChainInfo) {
924
- const queryEvents: any = {
925
- // acknowledge_packet과는 다르게 timeout_packet은 이전의 체인의 이벤트로부터만 알 수 있다.
926
- // 방법이 없기 때문에 여기서 이전의 체인으로부터 subscribe를 해서 이벤트를 받아야 한다.
927
- // 하지만 이 경우 ibc error tracking 로직에서 이것과 똑같은 subscription을 한번 더 하게 된다.
928
- // 이미 로직이 많이 복잡하기 때문에 로직을 덜 복잡하게 하기 위해서 이러한 비효율성(?)을 감수한다.
929
- // "timeout_packet.packet_src_port": targetChannel.portId,
930
- "timeout_packet.packet_src_channel": targetChannel.channelId,
931
- "timeout_packet.packet_sequence": targetChannel.sequence,
1192
+ if (call.calls && call.calls.length > 0) {
1193
+ searchForTransfers(call.calls);
1194
+ }
1195
+ }
932
1196
  };
933
1197
 
934
- const txTracer = new TendermintTxTracer(
935
- prevChainInfo.rpc,
936
- "/websocket"
937
- );
938
- closables.push(txTracer);
939
- txTracer.addEventListener("close", onCloseOnce);
940
- txTracer.addEventListener("error", onErrorOnce);
941
- txTracer.traceTx(queryEvents).then((res) => {
942
- txTracer.close();
943
-
944
- if (!res) {
945
- return;
946
- }
1198
+ searchForTransfers(traceRes.data.result.calls || []);
1199
+ }
947
1200
 
948
- // event가 발생한 시점에서 이미 timeout packet은 받은 상태이고
949
- // 경우 따로 정보를 얻을 필요는 없으므로 이후에 res를 쓰지는 않는다.
950
- // 위에 res null check는 사실 필요 없지만 혹시나 해서 넣어둔다.
951
- runInAction(() => {
952
- targetChannel.error = "Packet timeout";
953
- history.packetTimeout = true;
954
- onFulfillOnce();
955
- this.trackIBCPacketForwardingRecursive(id);
956
- });
1201
+ if (isFoundFromCall) {
1202
+ onResult({ success: true, resAmount: foundResAmount });
1203
+ return;
1204
+ }
1205
+
1206
+ // fallback to logs if debug_traceTransaction fails
1207
+ this.parseEVMTxReceiptLogs({
1208
+ txReceipt,
1209
+ recipient,
1210
+ targetChainId: chainId,
1211
+ targetDenom,
1212
+ onResult,
1213
+ });
1214
+ });
1215
+ })
1216
+ .finally(() => {
1217
+ onFulfill();
1218
+ });
1219
+ }
1220
+
1221
+ protected parseEVMTxReceiptLogs(params: {
1222
+ txReceipt: EthTxReceipt;
1223
+ recipient: string;
1224
+ targetChainId: string;
1225
+ targetDenom: string;
1226
+ onResult: (result: {
1227
+ success: boolean;
1228
+ resAmount?: { amount: string; denom: string }[];
1229
+ refundInfo?: {
1230
+ chainId: string;
1231
+ amount: { amount: string; denom: string }[];
1232
+ };
1233
+ error?: string;
1234
+ }) => void;
1235
+ }): void {
1236
+ const { txReceipt, recipient, targetChainId, targetDenom, onResult } =
1237
+ params;
1238
+
1239
+ const logs = txReceipt.logs;
1240
+ const transferTopic = id("Transfer(address,address,uint256)");
1241
+ const withdrawTopic = id("Withdrawal(address,uint256)");
1242
+ const hyperlaneReceiveTopic = id(
1243
+ "ReceivedTransferRemote(uint32,bytes32,uint256)"
1244
+ );
1245
+
1246
+ for (const log of logs) {
1247
+ if (log.topics[0] === transferTopic) {
1248
+ const to = "0x" + log.topics[2].slice(26);
1249
+ if (to.toLowerCase() === recipient.toLowerCase()) {
1250
+ const expectedAssetDenom = targetDenom.replace("erc20:", "");
1251
+ const amount = BigInt(log.data).toString(10);
1252
+
1253
+ if (log.address.toLowerCase() === expectedAssetDenom.toLowerCase()) {
1254
+ onResult({
1255
+ success: true,
1256
+ resAmount: [{ amount, denom: targetDenom }],
1257
+ });
1258
+ } else {
1259
+ console.log("refunded", log.address);
1260
+ // Transfer 토픽인 경우엔 ERC20의 tranfer 호출일텐데
1261
+ // 받을 토큰의 컨트랙트가 아닌 다른 컨트랙트에서 호출된 경우는 Swap을 실패한 것으로 추측
1262
+ // 고로 실제로 받은 토큰의 컨트랙트 주소로 환불 정보에 저장한다.
1263
+ onResult({
1264
+ success: false,
1265
+ error: "Swap failed",
1266
+ refundInfo: {
1267
+ chainId: targetChainId,
1268
+ amount: [
1269
+ {
1270
+ amount,
1271
+ denom: `erc20:${log.address.toLowerCase()}`,
1272
+ },
1273
+ ],
1274
+ },
957
1275
  });
958
1276
  }
1277
+ return;
1278
+ }
1279
+ } else if (log.topics[0] === withdrawTopic) {
1280
+ const to = "0x" + log.topics[1].slice(26);
1281
+ if (to.toLowerCase() === txReceipt.to?.toLowerCase()) {
1282
+ const amount = BigInt(log.data).toString(10);
1283
+ onResult({
1284
+ success: true,
1285
+ resAmount: [{ amount, denom: targetDenom }],
1286
+ });
1287
+ return;
1288
+ }
1289
+ } else if (log.topics[0] === hyperlaneReceiveTopic) {
1290
+ const to = "0x" + log.topics[2].slice(26);
1291
+ if (to.toLowerCase() === recipient.toLowerCase()) {
1292
+ const amount = BigInt(log.data).toString(10);
1293
+ // Hyperlane을 통해 Forma로 TIA를 받는 경우 토큰 수량이 decimal 6으로 기록되는데,
1294
+ // Forma에서는 decimal 18이기 때문에 12자리 만큼 0을 붙여준다.
1295
+ onResult({
1296
+ success: true,
1297
+ resAmount: [
1298
+ {
1299
+ amount:
1300
+ targetDenom === "forma-native"
1301
+ ? `${amount}000000000000`
1302
+ : amount,
1303
+ denom: targetDenom,
1304
+ },
1305
+ ],
1306
+ });
1307
+ return;
959
1308
  }
960
1309
  }
961
1310
  }
962
- };
963
1311
 
964
- getRecentSendHistories(chainId: string, type: string): RecentSendHistory[] {
965
- const key = `${ChainIdHelper.parse(chainId).identifier}/${type}`;
966
- return (this.recentSendHistoryMap.get(key) ?? []).slice(0, 20);
1312
+ // 결과를 찾지 못한 경우
1313
+ onResult({ success: false });
967
1314
  }
968
1315
 
969
- @action
970
- addRecentSendHistory(
971
- chainId: string,
972
- type: string,
973
- history: Omit<RecentSendHistory, "timestamp">
974
- ) {
975
- const key = `${ChainIdHelper.parse(chainId).identifier}/${type}`;
976
-
977
- let histories = this.recentSendHistoryMap.get(key) ?? [];
978
- histories.unshift({
979
- timestamp: Date.now(),
980
- ...history,
981
- });
982
- histories = histories.slice(0, 20);
983
-
984
- this.recentSendHistoryMap.set(key, histories);
985
- }
1316
+ // ============================================================================
1317
+ // Skip swap history
1318
+ // ============================================================================
986
1319
 
987
1320
  @action
988
- addRecentIBCTransferHistory(
989
- chainId: string,
1321
+ recordTxWithSkipSwap(
1322
+ sourceChainId: string,
990
1323
  destinationChainId: string,
1324
+ destinationAsset: {
1325
+ chainId: string;
1326
+ denom: string;
1327
+ expectedAmount: string;
1328
+ },
1329
+ simpleRoute: {
1330
+ isOnlyEvm: boolean;
1331
+ chainId: string;
1332
+ receiver: string;
1333
+ }[],
991
1334
  sender: string,
992
1335
  recipient: string,
993
1336
  amount: {
994
1337
  amount: string;
995
1338
  denom: string;
996
1339
  }[],
997
- memo: string,
998
- ibcChannels:
999
- | {
1000
- portId: string;
1001
- channelId: string;
1002
- counterpartyChainId: string;
1003
- }[],
1004
1340
  notificationInfo: {
1005
1341
  currencies: AppCurrency[];
1006
1342
  },
1007
- txHash: Uint8Array
1008
- ): string {
1009
- const id = (this.recentIBCHistorySeq++).toString();
1010
-
1011
- const history: IBCHistory = {
1012
- id,
1013
- chainId,
1014
- destinationChainId,
1015
- timestamp: Date.now(),
1016
- sender,
1017
- recipient,
1018
- amount,
1019
- memo,
1020
-
1021
- ibcHistory: ibcChannels.map((channel) => {
1022
- return {
1023
- portId: channel.portId,
1024
- channelId: channel.channelId,
1025
- counterpartyChainId: channel.counterpartyChainId,
1026
-
1027
- completed: false,
1028
- };
1029
- }),
1030
- notificationInfo,
1031
- txHash: Buffer.from(txHash).toString("hex"),
1032
- };
1033
-
1034
- this.recentIBCHistoryMap.set(id, history);
1035
-
1036
- return id;
1037
- }
1038
-
1039
- @action
1040
- addRecentIBCSwapHistory(
1041
- swapType: "amount-in" | "amount-out",
1042
- chainId: string,
1043
- destinationChainId: string,
1044
- sender: string,
1045
- amount: {
1046
- amount: string;
1047
- denom: string;
1048
- }[],
1049
- memo: string,
1050
- ibcChannels:
1051
- | {
1052
- portId: string;
1053
- channelId: string;
1054
- counterpartyChainId: string;
1055
- }[],
1056
- destinationAsset: {
1057
- chainId: string;
1058
- denom: string;
1059
- },
1060
- swapChannelIndex: number,
1061
- swapReceiver: string[],
1062
- notificationInfo: {
1063
- currencies: AppCurrency[];
1064
- },
1065
- txHash: Uint8Array
1066
- ): string {
1067
- const id = (this.recentIBCHistorySeq++).toString();
1068
-
1069
- const history: IBCHistory = {
1070
- id,
1071
- swapType,
1072
- chainId,
1073
- destinationChainId,
1074
- timestamp: Date.now(),
1075
- sender,
1076
- amount,
1077
- memo,
1078
-
1079
- ibcHistory: ibcChannels.map((channel) => {
1080
- return {
1081
- portId: channel.portId,
1082
- channelId: channel.channelId,
1083
- counterpartyChainId: channel.counterpartyChainId,
1084
-
1085
- completed: false,
1086
- };
1087
- }),
1088
- destinationAsset,
1089
- swapChannelIndex,
1090
- swapReceiver,
1091
- resAmount: [],
1092
- notificationInfo,
1093
- txHash: Buffer.from(txHash).toString("hex"),
1094
- };
1095
-
1096
- this.recentIBCHistoryMap.set(id, history);
1097
-
1098
- return id;
1099
- }
1100
-
1101
- getRecentIBCHistory(id: string): IBCHistory | undefined {
1102
- return this.recentIBCHistoryMap.get(id);
1103
- }
1104
-
1105
- getRecentIBCHistories(): IBCHistory[] {
1106
- return Array.from(this.recentIBCHistoryMap.values()).filter((history) => {
1107
- if (!this.chainsService.hasChainInfo(history.chainId)) {
1108
- return false;
1109
- }
1110
-
1111
- if (!this.chainsService.hasChainInfo(history.destinationChainId)) {
1112
- return false;
1113
- }
1114
-
1115
- if (
1116
- history.ibcHistory.some((history) => {
1117
- return !this.chainsService.hasChainInfo(history.counterpartyChainId);
1118
- })
1119
- ) {
1120
- return false;
1121
- }
1122
-
1123
- return true;
1124
- });
1125
- }
1126
-
1127
- @action
1128
- removeRecentIBCHistory(id: string): boolean {
1129
- return this.recentIBCHistoryMap.delete(id);
1130
- }
1131
-
1132
- @action
1133
- clearAllRecentIBCHistory(): void {
1134
- this.recentIBCHistoryMap.clear();
1135
- }
1136
-
1137
- // skip related methods
1138
- @action
1139
- recordTxWithSkipSwap(
1140
- sourceChainId: string,
1141
- destinationChainId: string,
1142
- destinationAsset: {
1143
- chainId: string;
1144
- denom: string;
1145
- expectedAmount: string;
1146
- },
1147
- simpleRoute: {
1148
- isOnlyEvm: boolean;
1149
- chainId: string;
1150
- receiver: string;
1151
- }[],
1152
- sender: string,
1153
- recipient: string,
1154
- amount: {
1155
- amount: string;
1156
- denom: string;
1157
- }[],
1158
- notificationInfo: {
1159
- currencies: AppCurrency[];
1160
- },
1161
- routeDurationSeconds: number = 0,
1162
- txHash: string,
1163
- isOnlyUseBridge?: boolean
1343
+ routeDurationSeconds: number = 0,
1344
+ txHash: string,
1345
+ isOnlyUseBridge?: boolean
1164
1346
  ): string {
1165
1347
  const id = (this.recentIBCHistorySeq++).toString();
1166
1348
 
@@ -1224,34 +1406,49 @@ export class RecentSendHistoryService {
1224
1406
  retry(
1225
1407
  () => {
1226
1408
  return new Promise<void>((txFulfilledResolve, txFulfilledReject) => {
1227
- this.checkAndTrackSkipSwapTxFulfilledRecursive(
1228
- history,
1229
- (keepTracking: boolean) => {
1230
- txFulfilledResolve();
1231
-
1232
- if (!keepTracking) {
1233
- return;
1234
- }
1409
+ this.checkAndTrackSwapTxFulfilledRecursive({
1410
+ chainId: history.chainId,
1411
+ txHash: history.txHash,
1412
+ onSuccess: () => {
1413
+ this.requestSkipTxTrackInternal({
1414
+ chainId: history.chainId,
1415
+ txHash: history.txHash,
1416
+ onRemoveHistory: () => this.removeRecentSkipHistory(id),
1417
+ onFulfill: (keepTracking: boolean) => {
1418
+ txFulfilledResolve();
1419
+
1420
+ if (!keepTracking) {
1421
+ return;
1422
+ }
1235
1423
 
1236
- retry(
1237
- () => {
1238
- return new Promise<void>((resolve, reject) => {
1239
- this.checkAndUpdateSkipSwapHistoryRecursive(
1240
- id,
1241
- resolve,
1242
- reject
1243
- );
1244
- });
1424
+ retry(
1425
+ () => {
1426
+ return new Promise<void>((resolve, reject) => {
1427
+ this.checkAndUpdateSkipSwapHistoryRecursive(
1428
+ id,
1429
+ resolve,
1430
+ reject
1431
+ );
1432
+ });
1433
+ },
1434
+ {
1435
+ maxRetries: 50,
1436
+ waitMsAfterError: 500,
1437
+ maxWaitMsAfterError: 15000,
1438
+ }
1439
+ );
1245
1440
  },
1246
- {
1247
- maxRetries: 50,
1248
- waitMsAfterError: 500,
1249
- maxWaitMsAfterError: 15000,
1250
- }
1251
- );
1441
+ });
1252
1442
  },
1253
- txFulfilledReject
1254
- );
1443
+ onPending: txFulfilledReject,
1444
+ onFailed: () => {
1445
+ this.removeRecentSkipHistory(id);
1446
+ txFulfilledResolve();
1447
+ },
1448
+ onError: () => {
1449
+ txFulfilledResolve();
1450
+ },
1451
+ });
1255
1452
  });
1256
1453
  },
1257
1454
  {
@@ -1262,161 +1459,34 @@ export class RecentSendHistoryService {
1262
1459
  );
1263
1460
  }
1264
1461
 
1265
- protected checkAndTrackSkipSwapTxFulfilledRecursive = (
1266
- history: SkipHistory,
1267
- onFulfill: (keepTracking: boolean) => void,
1268
- onError: () => void
1269
- ): void => {
1270
- const chainInfo = this.chainsService.getChainInfo(history.chainId);
1271
- if (!chainInfo) {
1272
- onFulfill(false);
1273
- return;
1274
- }
1275
-
1276
- if (this.chainsService.isEvmChain(history.chainId)) {
1277
- const evmInfo = chainInfo.evm;
1278
- if (!evmInfo) {
1279
- onFulfill(false);
1280
- return;
1281
- }
1282
-
1283
- simpleFetch<{
1284
- result: EthTxReceipt | null;
1285
- error?: Error;
1286
- }>(evmInfo.rpc, {
1287
- method: "POST",
1288
- headers: {
1289
- "content-type": "application/json",
1290
- "request-source": origin,
1291
- },
1292
- body: JSON.stringify({
1293
- jsonrpc: "2.0",
1294
- method: "eth_getTransactionReceipt",
1295
- params: [history.txHash],
1296
- id: 1,
1297
- }),
1462
+ protected requestSkipTxTrackInternal(params: {
1463
+ chainId: string;
1464
+ txHash: string;
1465
+ onFulfill: (keepTracking: boolean) => void;
1466
+ onRemoveHistory: () => void;
1467
+ }) {
1468
+ const { chainId, txHash, onFulfill, onRemoveHistory } = params;
1469
+ const chainIdForApi = this.chainsService.isEvmChain(chainId)
1470
+ ? chainId.replace("eip155:", "")
1471
+ : chainId;
1472
+
1473
+ setTimeout(() => {
1474
+ requestSkipTxTrack({
1475
+ endpoint: SWAP_API_ENDPOINT,
1476
+ chainId: chainIdForApi,
1477
+ txHash,
1298
1478
  })
1299
- .then((res) => {
1300
- const txReceipt = res.data.result;
1301
- if (txReceipt) {
1302
- if (txReceipt.status === EthTxStatus.Success) {
1303
- setTimeout(() => {
1304
- simpleFetch(SWAP_API_ENDPOINT, "/v1/swap/tx", {
1305
- method: "POST",
1306
- headers: {
1307
- "content-type": "application/json",
1308
- ...(() => {
1309
- const res: {
1310
- authorization?: string;
1311
- } = {};
1312
- if (process.env["SKIP_API_KEY"]) {
1313
- res.authorization = process.env["SKIP_API_KEY"];
1314
- }
1315
- return res;
1316
- })(),
1317
- },
1318
- body: JSON.stringify({
1319
- tx_hash: history.txHash,
1320
- chain_id: history.chainId.replace("eip155:", ""),
1321
- }),
1322
- })
1323
- .then((result) => {
1324
- console.log(
1325
- `Skip tx track result: ${JSON.stringify(result)}`
1326
- );
1327
- onFulfill(true);
1328
- })
1329
- .catch((e) => {
1330
- console.log(e);
1331
- this.removeRecentSkipHistory(history.id);
1332
- onFulfill(false);
1333
- });
1334
- }, 2000);
1335
- } else {
1336
- // tx가 실패한거면 종료
1337
- this.removeRecentSkipHistory(history.id);
1338
- onFulfill(false);
1339
- }
1340
- } else {
1341
- onError();
1342
- }
1343
- })
1344
- .catch(() => {
1345
- // 오류가 발생하면 종료
1346
- onFulfill(false);
1347
- });
1348
- } else {
1349
- const txTracer = new TendermintTxTracer(chainInfo.rpc, "/websocket");
1350
- txTracer.addEventListener("error", () => onFulfill(false));
1351
- txTracer
1352
- .traceTx(Buffer.from(history.txHash.replace("0x", ""), "hex"))
1353
- .then((res: any) => {
1354
- txTracer.close();
1355
-
1356
- let txResult;
1357
-
1358
- if (Array.isArray(res.txs)) {
1359
- if (res.txs && res.txs.length > 0) {
1360
- txResult = res.txs[0].tx_result;
1361
- } else {
1362
- // In case tx is not confirmed, just wait for next check
1363
- onError();
1364
- return;
1365
- }
1366
- } else {
1367
- txResult = res;
1368
- }
1369
-
1370
- if (!txResult || typeof txResult.code !== "number") {
1371
- onError();
1372
- return;
1373
- }
1374
-
1375
- if (txResult.code === 0) {
1376
- setTimeout(() => {
1377
- simpleFetch(SWAP_API_ENDPOINT, "/v1/swap/tx", {
1378
- method: "POST",
1379
- headers: {
1380
- "content-type": "application/json",
1381
- ...(() => {
1382
- const res: {
1383
- authorization?: string;
1384
- } = {};
1385
- if (process.env["SKIP_API_KEY"]) {
1386
- res.authorization = process.env["SKIP_API_KEY"];
1387
- }
1388
- return res;
1389
- })(),
1390
- },
1391
- body: JSON.stringify({
1392
- tx_hash: history.txHash,
1393
- chain_id: history.chainId,
1394
- }),
1395
- })
1396
- .then((result) => {
1397
- console.log(
1398
- `Skip tx track result: ${JSON.stringify(result)}`
1399
- );
1400
- onFulfill(true);
1401
- })
1402
- .catch((e) => {
1403
- console.log(e);
1404
- this.removeRecentSkipHistory(history.id);
1405
- onFulfill(false);
1406
- });
1407
- }, 2000);
1408
- } else {
1409
- // tx가 실패한거면 종료
1410
- this.removeRecentSkipHistory(history.id);
1411
- onFulfill(false);
1412
- }
1479
+ .then((result) => {
1480
+ console.log(`Skip tx track result: ${JSON.stringify(result)}`);
1481
+ onFulfill(true);
1413
1482
  })
1414
- .catch(() => {
1415
- // 오류가 발생하면 종료
1483
+ .catch((e) => {
1484
+ console.log(e);
1485
+ onRemoveHistory();
1416
1486
  onFulfill(false);
1417
1487
  });
1418
- }
1419
- };
1488
+ }, 2000);
1489
+ }
1420
1490
 
1421
1491
  protected checkAndUpdateSkipSwapHistoryRecursive = (
1422
1492
  id: string,
@@ -1454,30 +1524,11 @@ export class RecentSendHistoryService {
1454
1524
  return;
1455
1525
  }
1456
1526
 
1457
- // Skip API에 보낼 request 정보
1458
- const request: StatusRequest = {
1459
- tx_hash: txHash,
1460
- chain_id: chainId.replace("eip155:", ""),
1461
- };
1462
- const requestParams = new URLSearchParams(request).toString();
1463
-
1464
- simpleFetch<TxStatusResponse>(
1465
- SWAP_API_ENDPOINT,
1466
- `/v1/swap/tx?${requestParams}`,
1467
- {
1468
- method: "GET",
1469
- headers: {
1470
- "content-type": "application/json",
1471
- ...(() => {
1472
- const res: { authorization?: string } = {};
1473
- if (process.env["SKIP_API_KEY"]) {
1474
- res.authorization = process.env["SKIP_API_KEY"];
1475
- }
1476
- return res;
1477
- })(),
1478
- },
1479
- }
1480
- )
1527
+ requestSkipTxStatus({
1528
+ endpoint: SWAP_API_ENDPOINT,
1529
+ chainId: chainId.replace("eip155:", ""),
1530
+ txHash,
1531
+ })
1481
1532
  .then((res) => {
1482
1533
  const {
1483
1534
  state,
@@ -1760,265 +1811,1815 @@ export class RecentSendHistoryService {
1760
1811
  // 최종 routeIndex 갱신
1761
1812
  history.routeIndex = nextRouteIndex;
1762
1813
 
1763
- // state에 따라 트래킹 완료/재시도 결정
1764
- switch (state) {
1765
- case "STATE_ABANDONED":
1766
- case "STATE_COMPLETED_ERROR":
1767
- case "STATE_COMPLETED_SUCCESS":
1768
- // 성공 상태인데 라우트가 남았다면 마지막 라우트로 이동
1769
- if (
1770
- state === "STATE_COMPLETED_SUCCESS" &&
1771
- nextRouteIndex !== simpleRoute.length - 1
1772
- ) {
1773
- history.routeIndex = simpleRoute.length - 1;
1774
- }
1814
+ // state에 따라 트래킹 완료/재시도 결정
1815
+ switch (state) {
1816
+ case "STATE_ABANDONED":
1817
+ case "STATE_COMPLETED_ERROR":
1818
+ case "STATE_COMPLETED_SUCCESS":
1819
+ // 성공 상태인데 라우트가 남았다면 마지막 라우트로 이동
1820
+ if (
1821
+ state === "STATE_COMPLETED_SUCCESS" &&
1822
+ nextRouteIndex !== simpleRoute.length - 1
1823
+ ) {
1824
+ history.routeIndex = simpleRoute.length - 1;
1825
+ }
1826
+
1827
+ if (receiveTxHash) {
1828
+ this.trackSkipDestinationAssetAmount(
1829
+ id,
1830
+ receiveTxHash,
1831
+ onFulfill
1832
+ );
1833
+ } else {
1834
+ history.trackDone = true;
1835
+ onFulfill();
1836
+ }
1837
+ break;
1838
+
1839
+ case "STATE_PENDING":
1840
+ case "STATE_PENDING_ERROR":
1841
+ // 아직 트래킹 중이거나 에러 상태 전파 중 => 재시도
1842
+ onError();
1843
+ break;
1844
+ }
1845
+ })
1846
+ .catch((e) => {
1847
+ console.error(e);
1848
+ onError();
1849
+ });
1850
+ };
1851
+
1852
+ protected trackSkipDestinationAssetAmount(
1853
+ historyId: string,
1854
+ txHash: string,
1855
+ onFulfill: () => void
1856
+ ) {
1857
+ const history = this.getRecentSkipHistory(historyId);
1858
+ if (!history) {
1859
+ onFulfill();
1860
+ return;
1861
+ }
1862
+
1863
+ const chainInfo = this.chainsService.getChainInfo(
1864
+ history.destinationChainId
1865
+ );
1866
+ if (!chainInfo) {
1867
+ onFulfill();
1868
+ return;
1869
+ }
1870
+
1871
+ this.trackDestinationAssetAmount({
1872
+ chainId: history.destinationChainId,
1873
+ txHash,
1874
+ recipient: history.recipient,
1875
+ targetDenom: history.destinationAsset.denom,
1876
+ onResult: (resAmount) => {
1877
+ runInAction(() => {
1878
+ history.resAmount.push(resAmount);
1879
+ history.trackDone = true;
1880
+ });
1881
+ },
1882
+ onRefund: (refundInfo, error) => {
1883
+ runInAction(() => {
1884
+ history.trackError = error;
1885
+ history.swapRefundInfo = refundInfo;
1886
+ history.trackDone = true;
1887
+ });
1888
+ },
1889
+ onFulfill: () => {
1890
+ // ensure completion even if no result parsed
1891
+ runInAction(() => {
1892
+ history.trackDone = true;
1893
+ });
1894
+ onFulfill();
1895
+ },
1896
+ });
1897
+ }
1898
+
1899
+ @action
1900
+ removeRecentSkipHistory(id: string): boolean {
1901
+ return this.recentSkipHistoryMap.delete(id);
1902
+ }
1903
+
1904
+ @action
1905
+ clearAllRecentSkipHistory(): void {
1906
+ this.recentSkipHistoryMap.clear();
1907
+ }
1908
+
1909
+ // ============================================================================
1910
+ // Swap V2 history
1911
+ // ============================================================================
1912
+
1913
+ @action
1914
+ recordTxWithSwapV2(
1915
+ fromChainId: string,
1916
+ toChainId: string,
1917
+ provider: SwapProvider,
1918
+ destinationAsset: {
1919
+ chainId: string;
1920
+ denom: string;
1921
+ expectedAmount: string;
1922
+ },
1923
+ simpleRoute: {
1924
+ isOnlyEvm: boolean;
1925
+ chainId: string;
1926
+ receiver: string;
1927
+ }[],
1928
+ sender: string,
1929
+ recipient: string,
1930
+ amount: {
1931
+ amount: string;
1932
+ denom: string;
1933
+ }[],
1934
+ notificationInfo: {
1935
+ currencies: AppCurrency[];
1936
+ },
1937
+ routeDurationSeconds: number = 0,
1938
+ txHash: string,
1939
+ isOnlyUseBridge?: boolean,
1940
+ backgroundExecutionId?: string
1941
+ ): string {
1942
+ const id = (this.recentSwapV2HistorySeq++).toString();
1943
+
1944
+ const history: SwapV2History = {
1945
+ id,
1946
+ fromChainId,
1947
+ toChainId,
1948
+ provider,
1949
+ timestamp: Date.now(),
1950
+ sender,
1951
+ recipient,
1952
+ amount,
1953
+ notificationInfo,
1954
+ routeDurationSeconds,
1955
+ txHash,
1956
+ isOnlyUseBridge,
1957
+ status: SwapV2TxStatus.IN_PROGRESS,
1958
+ simpleRoute,
1959
+ routeIndex: -1,
1960
+ destinationAsset,
1961
+ resAmount: [],
1962
+ assetLocationInfo: undefined,
1963
+ notified: undefined,
1964
+ backgroundExecutionId,
1965
+ };
1966
+
1967
+ this.recentSwapV2HistoryMap.set(id, history);
1968
+ this.trackSwapV2Recursive(id);
1969
+
1970
+ return id;
1971
+ }
1972
+
1973
+ trackSwapV2Recursive(id: string): void {
1974
+ const history = this.getRecentSwapV2History(id);
1975
+ if (!history) {
1976
+ return;
1977
+ }
1978
+
1979
+ retry(
1980
+ () => {
1981
+ return new Promise<void>((txFulfilledResolve, txFulfilledReject) => {
1982
+ this.checkAndTrackSwapTxFulfilledRecursive({
1983
+ chainId: history.fromChainId,
1984
+ txHash: history.txHash,
1985
+ onSuccess: () => {
1986
+ txFulfilledResolve();
1987
+
1988
+ retry(
1989
+ () => {
1990
+ return new Promise<void>((resolve, reject) => {
1991
+ this.checkAndUpdateSwapV2HistoryRecursive(
1992
+ id,
1993
+ resolve,
1994
+ reject
1995
+ );
1996
+ });
1997
+ },
1998
+ {
1999
+ maxRetries: 60,
2000
+ waitMsAfterError: 1000,
2001
+ maxWaitMsAfterError: 45000,
2002
+ }
2003
+ );
2004
+ },
2005
+ onPending: txFulfilledReject,
2006
+ onFailed: () => {
2007
+ this.removeRecentSwapV2History(id);
2008
+ txFulfilledResolve();
2009
+ },
2010
+ onError: txFulfilledResolve,
2011
+ });
2012
+ });
2013
+ },
2014
+ {
2015
+ maxRetries: 60,
2016
+ waitMsAfterError: 1000,
2017
+ maxWaitMsAfterError: 45000,
2018
+ }
2019
+ );
2020
+ }
2021
+
2022
+ protected checkAndUpdateSwapV2HistoryRecursive(
2023
+ id: string,
2024
+ onFulfill: () => void,
2025
+ onError: () => void
2026
+ ): void {
2027
+ const history = this.getRecentSwapV2History(id);
2028
+ if (!history) {
2029
+ onFulfill();
2030
+ return;
2031
+ }
2032
+
2033
+ // if already tracked, fulfill
2034
+ if (history.trackDone) {
2035
+ onFulfill();
2036
+ return;
2037
+ }
2038
+
2039
+ const { txHash, fromChainId, toChainId, provider } = history;
2040
+
2041
+ const normalizeChainId = (chainId: string): string => {
2042
+ return chainId.replace("eip155:", "");
2043
+ };
2044
+
2045
+ requestSwapV2TxStatus({
2046
+ endpoint: SWAP_API_ENDPOINT,
2047
+ fromChainId: normalizeChainId(fromChainId),
2048
+ toChainId: normalizeChainId(toChainId),
2049
+ provider,
2050
+ txHash,
2051
+ })
2052
+ .then((res) => {
2053
+ this.processSwapV2StatusResponse(id, res.data, onFulfill, onError);
2054
+ })
2055
+ .catch((e) => {
2056
+ console.error("SwapV2 status tracking error:", e);
2057
+ onError();
2058
+ });
2059
+ }
2060
+
2061
+ @action
2062
+ protected processSwapV2StatusResponse(
2063
+ id: string,
2064
+ response: SwapV2TxStatusResponse,
2065
+ onFulfill: () => void,
2066
+ onError: () => void
2067
+ ): void {
2068
+ const history = this.getRecentSwapV2History(id);
2069
+ if (!history) {
2070
+ onFulfill();
2071
+ return;
2072
+ }
2073
+
2074
+ const { status, steps, asset_location } = response;
2075
+ const { simpleRoute } = history;
2076
+ const prevRouteIndex = history.routeIndex;
2077
+
2078
+ // 모든 상태 즉시 업데이트 (UNKNOWN 포함)
2079
+ history.status = status;
2080
+ history.trackError = undefined;
2081
+
2082
+ // This might be the state where tracking has just started,
2083
+ // so handle the error and retry
2084
+ if (!steps || steps.length === 0) {
2085
+ if (
2086
+ status === SwapV2TxStatus.IN_PROGRESS ||
2087
+ status === SwapV2TxStatus.UNKNOWN
2088
+ ) {
2089
+ onError();
2090
+ } else {
2091
+ // swap on single evm chain might not have steps
2092
+ history.trackDone = true;
2093
+ onFulfill();
2094
+ }
2095
+ return;
2096
+ }
2097
+
2098
+ // find current step (not success first, otherwise last step)
2099
+ const currentStep =
2100
+ steps.find((s) => s.status !== SwapV2RouteStepStatus.SUCCESS) ??
2101
+ steps[steps.length - 1];
2102
+
2103
+ const normalizeChainId = (chainId: string): string => {
2104
+ return chainId.replace("eip155:", "").toLowerCase();
2105
+ };
2106
+
2107
+ // Find the LAST step that matches the actual destination chain (history.toChainId)
2108
+ // Use reverse + find to handle routes that visit the destination chain multiple times
2109
+ // This handles cases where intermediate steps fail but the final destination is reached
2110
+ const destinationStep = [...steps].reverse().find((s) => {
2111
+ if (!s.chain_id) {
2112
+ return false;
2113
+ }
2114
+ return (
2115
+ normalizeChainId(s.chain_id) === normalizeChainId(history.toChainId)
2116
+ );
2117
+ });
2118
+ const isDestinationStepSuccessful =
2119
+ destinationStep &&
2120
+ destinationStep.status === SwapV2RouteStepStatus.SUCCESS &&
2121
+ !!destinationStep.tx_hash;
2122
+
2123
+ const findSimpleRouteIndex = (chainId: string): number => {
2124
+ const normalizedChainId = normalizeChainId(chainId);
2125
+ for (let i = 0; i < simpleRoute.length; i++) {
2126
+ const routeChainId = normalizeChainId(simpleRoute[i].chainId);
2127
+ if (routeChainId === normalizedChainId) {
2128
+ return i;
2129
+ }
2130
+ }
2131
+ return -1;
2132
+ };
2133
+
2134
+ // NOTE: The lengths of simpleRoute and steps may differ.
2135
+ let updatedRouteIndex = Math.max(0, history.routeIndex);
2136
+
2137
+ // 1. Find highest completed simpleRoute index from all SUCCESS steps
2138
+ let highestCompletedIndex = -1;
2139
+ for (const step of steps) {
2140
+ if (step.status === SwapV2RouteStepStatus.SUCCESS && step.chain_id) {
2141
+ const routeIdx = findSimpleRouteIndex(step.chain_id);
2142
+ if (routeIdx > highestCompletedIndex) {
2143
+ highestCompletedIndex = routeIdx;
2144
+ }
2145
+ }
2146
+ }
2147
+
2148
+ // 2. Also check if currentStep is in simpleRoute
2149
+ let currentStepIndex = -1;
2150
+ if (currentStep.chain_id) {
2151
+ currentStepIndex = findSimpleRouteIndex(currentStep.chain_id);
2152
+ }
2153
+
2154
+ // 3. Use the higher value as updatedRouteIndex
2155
+ const candidateIndex = Math.max(highestCompletedIndex, currentStepIndex);
2156
+ if (candidateIndex >= 0) {
2157
+ updatedRouteIndex = candidateIndex;
2158
+ }
2159
+
2160
+ const publishExecutableChains = (chainIds?: string[]) => {
2161
+ if (!history.backgroundExecutionId) {
2162
+ return;
2163
+ }
2164
+ const executableChainIds =
2165
+ chainIds ?? this.getExecutableChainIdsFromSwapV2History(history);
2166
+ this.publisher.publish({
2167
+ type: "executable",
2168
+ executionId: history.backgroundExecutionId,
2169
+ executableChainIds,
2170
+ });
2171
+ };
2172
+
2173
+ const isUnknownStatus =
2174
+ status === SwapV2TxStatus.UNKNOWN ||
2175
+ steps.some((s) => s.status === SwapV2RouteStepStatus.UNKNOWN);
2176
+
2177
+ if (isUnknownStatus) {
2178
+ if (!history.unknownStatusFirstSeenAt) {
2179
+ // UNKNOWN 상태 처음 발견 - 타임스탬프 기록
2180
+ history.unknownStatusFirstSeenAt = Date.now();
2181
+ } else {
2182
+ const elapsedMs = Date.now() - history.unknownStatusFirstSeenAt;
2183
+ if (elapsedMs >= UNKNOWN_TX_STATUS_TIMEOUT_MS) {
2184
+ this.analyticsService.logEventIgnoreError(
2185
+ "swapV2UnknownTxStatusWithTimeout",
2186
+ {
2187
+ provider: history.provider,
2188
+ fromChainId: history.fromChainId,
2189
+ toChainId: history.toChainId,
2190
+ txHash: history.txHash,
2191
+ executedAt: history.timestamp,
2192
+ }
2193
+ );
2194
+
2195
+ history.trackDone = true;
2196
+ onFulfill();
2197
+ return;
2198
+ }
2199
+ }
2200
+ } else {
2201
+ // UNKNOWN 상태가 아니면 타임스탬프 초기화
2202
+ if (history.unknownStatusFirstSeenAt !== undefined) {
2203
+ history.unknownStatusFirstSeenAt = undefined;
2204
+ }
2205
+ }
2206
+
2207
+ switch (status) {
2208
+ case SwapV2TxStatus.IN_PROGRESS:
2209
+ case SwapV2TxStatus.UNKNOWN:
2210
+ // publish executable chains if routeIndex increased
2211
+ if (updatedRouteIndex > prevRouteIndex) {
2212
+ history.routeIndex = updatedRouteIndex;
2213
+ publishExecutableChains();
2214
+ }
2215
+ // Continue polling
2216
+ onError();
2217
+ break;
2218
+
2219
+ case SwapV2TxStatus.SUCCESS:
2220
+ case SwapV2TxStatus.PARTIAL_SUCCESS:
2221
+ case SwapV2TxStatus.FAILED:
2222
+ // If current step is still in progress, retry a few more times before finalizing
2223
+ if (currentStep.status === SwapV2RouteStepStatus.IN_PROGRESS) {
2224
+ const maxRetries = 3;
2225
+ const retryCount = history.finalizationRetryCount ?? 0;
2226
+
2227
+ if (retryCount < maxRetries) {
2228
+ history.finalizationRetryCount = retryCount + 1;
2229
+ onError();
2230
+ break;
2231
+ }
2232
+ // Max retries reached, fall through to finalize
2233
+ }
2234
+
2235
+ let executableChainIdsToPublish: string[] | undefined;
2236
+
2237
+ // NOTE: 현재 asset_location은 skip의 multichain operation인 경우에만 주어지는 값이다.
2238
+ if (asset_location) {
2239
+ const chainId = asset_location.chain_id;
2240
+ const evmLikeChainId = Number(chainId);
2241
+ const isEVMChainId =
2242
+ !Number.isNaN(evmLikeChainId) && evmLikeChainId > 0;
2243
+ const chainIdInKeplr = isEVMChainId ? `eip155:${chainId}` : chainId;
2244
+ const denomInKeplr = isEVMChainId
2245
+ ? `erc20:${asset_location.denom}`
2246
+ : asset_location.denom;
2247
+
2248
+ // destination chain에 destination denom으로 도착했으면 완전 성공이므로
2249
+ // assetLocationInfo를 설정하지 않음
2250
+ const isDestinationReached =
2251
+ chainIdInKeplr === history.toChainId &&
2252
+ denomInKeplr.toLowerCase() ===
2253
+ history.destinationAsset.denom.toLowerCase();
2254
+ if (isDestinationReached) {
2255
+ history.routeIndex = simpleRoute.length - 1;
2256
+ executableChainIdsToPublish =
2257
+ this.getExecutableChainIdsFromSwapV2History(history, true);
2258
+ history.resAmount.push([
2259
+ {
2260
+ amount: asset_location.amount,
2261
+ denom: denomInKeplr,
2262
+ },
2263
+ ]);
2264
+
2265
+ this.notifySwapV2Success(history);
2266
+ } else {
2267
+ /*
2268
+ Determine the type of asset location:
2269
+ - "intermediate": SUCCESS 상태이지만 asset_location이 최종 목적지가 아닌 경우
2270
+ (예: base USDC -> osmosis OSMO 스왑 시, noble USDC가 먼저 도착하고
2271
+ 이후 noble USDC -> osmosis OSMO로 ibc swap하는 transaction이 필요한 경우)
2272
+ 이 경우 추가 transaction을 실행하거나 현재 받은 자산을 그대로 둘 수 있음
2273
+ - "refund": 중간에서 또는 destination에서 스왑 실패 등으로 destination asset이 아닌 자산이 릴리즈된 경우
2274
+ backgroundExecutionId가 있으면 멀티 transaction 케이스이므로 다음 transaction을 실행할 수 있도록 'intermediate'로 설정
2275
+ */
2276
+ const assetLocationType: "refund" | "intermediate" =
2277
+ status === SwapV2TxStatus.SUCCESS && history.backgroundExecutionId
2278
+ ? "intermediate"
2279
+ : "refund";
2280
+
2281
+ history.assetLocationInfo = {
2282
+ chainId: chainIdInKeplr,
2283
+ amount: [
2284
+ {
2285
+ amount: asset_location.amount,
2286
+ denom: denomInKeplr,
2287
+ },
2288
+ ],
2289
+ type: assetLocationType,
2290
+ };
2291
+
2292
+ // refund 타입인 경우 status도 FAILED로 변경하여 UI에서 refund 상황을 표시할 수 있도록 함
2293
+ if (assetLocationType === "refund") {
2294
+ history.status = SwapV2TxStatus.FAILED;
2295
+ }
2296
+
2297
+ // asset location chain까지 routeIndex가 이동해야 하는지 확인
2298
+ const assetLocationChainIndex = simpleRoute.findIndex(
2299
+ (route) => route.chainId === chainIdInKeplr
2300
+ );
2301
+ if (
2302
+ assetLocationChainIndex !== -1 &&
2303
+ assetLocationChainIndex > updatedRouteIndex
2304
+ ) {
2305
+ history.routeIndex = assetLocationChainIndex;
2306
+ }
2307
+ executableChainIdsToPublish =
2308
+ this.getExecutableChainIdsFromSwapV2History(history);
2309
+ }
2310
+ } else if (status === SwapV2TxStatus.SUCCESS) {
2311
+ // For SUCCESS without asset_location, move routeIndex to end
2312
+ history.routeIndex = simpleRoute.length - 1;
2313
+ executableChainIdsToPublish =
2314
+ this.getExecutableChainIdsFromSwapV2History(history, true);
2315
+
2316
+ this.notifySwapV2Success(history);
2317
+ }
2318
+
2319
+ // Publish executable chains
2320
+ publishExecutableChains(executableChainIdsToPublish);
2321
+
2322
+ // destination 체인/denom 기준으로만 추가 추적을 시도한다.
2323
+ const targetChainId = history.toChainId;
2324
+ const targetDenom = history.destinationAsset.denom;
2325
+
2326
+ // 해당 위치의 tx_hash를 찾아서 자산 추적, 없을 수도 있다.
2327
+ const targetTxHash = destinationStep?.tx_hash ?? currentStep.tx_hash;
2328
+
2329
+ const isAtDestinationChain = (() => {
2330
+ // Top-level SUCCESS + destination step successful → check against destination step
2331
+ if (
2332
+ status === SwapV2TxStatus.SUCCESS &&
2333
+ isDestinationStepSuccessful &&
2334
+ destinationStep?.chain_id
2335
+ ) {
2336
+ return (
2337
+ normalizeChainId(destinationStep.chain_id) ===
2338
+ normalizeChainId(targetChainId)
2339
+ );
2340
+ }
2341
+
2342
+ // Default logic (other cases)
2343
+ if (!currentStep.chain_id) {
2344
+ return false;
2345
+ }
2346
+ return (
2347
+ normalizeChainId(currentStep.chain_id) ===
2348
+ normalizeChainId(targetChainId)
2349
+ );
2350
+ })();
2351
+
2352
+ const skipAssetTracking =
2353
+ history.resAmount.length > 0 || history.assetLocationInfo != null;
2354
+
2355
+ // resAmount 또는 assetLocationInfo가 없으면 추가적으로 자산 추적을 해야 한다.
2356
+ if (targetTxHash && !skipAssetTracking && isAtDestinationChain) {
2357
+ console.log("trackSwapV2ReleasedAssetAmount", id, targetTxHash);
2358
+
2359
+ this.trackSwapV2ReleasedAssetAmount(
2360
+ id,
2361
+ targetTxHash,
2362
+ targetChainId,
2363
+ targetDenom,
2364
+ onFulfill
2365
+ );
2366
+ } else if (
2367
+ status === SwapV2TxStatus.SUCCESS &&
2368
+ !skipAssetTracking &&
2369
+ !isAtDestinationChain
2370
+ ) {
2371
+ // response status는 SUCCESS인데 destination chain에 도달하지 않음 → 실패 처리
2372
+ runInAction(() => {
2373
+ history.status = SwapV2TxStatus.FAILED;
2374
+ history.trackDone = true;
2375
+ });
2376
+
2377
+ // TODO: additional tracking for failed case...
2378
+
2379
+ onFulfill();
2380
+ } else {
2381
+ history.trackDone = true;
2382
+ onFulfill();
2383
+ }
2384
+ break;
2385
+ }
2386
+ }
2387
+
2388
+ /**
2389
+ * Track released asset amount from tx receipt.
2390
+ * - SUCCESS: destination asset 추적
2391
+ * - FAILED/PARTIAL_SUCCESS + assetLocationInfo: refund된 자산 추적
2392
+ */
2393
+ protected trackSwapV2ReleasedAssetAmount(
2394
+ historyId: string,
2395
+ txHash: string,
2396
+ targetChainId: string,
2397
+ targetDenom: string,
2398
+ onFulfill: () => void
2399
+ ) {
2400
+ const history = this.getRecentSwapV2History(historyId);
2401
+ if (!history) {
2402
+ onFulfill();
2403
+ return;
2404
+ }
2405
+
2406
+ const chainInfo = this.chainsService.getChainInfo(targetChainId);
2407
+ if (!chainInfo) {
2408
+ onFulfill();
2409
+ return;
2410
+ }
2411
+
2412
+ this.trackDestinationAssetAmount({
2413
+ chainId: targetChainId,
2414
+ txHash,
2415
+ recipient: history.recipient,
2416
+ targetDenom,
2417
+ onResult: (resAmount) => {
2418
+ runInAction(() => {
2419
+ history.resAmount.push(resAmount);
2420
+ history.trackDone = true;
2421
+
2422
+ this.notifySwapV2Success(history);
2423
+ });
2424
+ },
2425
+ onRefund: (refundInfo, error) => {
2426
+ runInAction(() => {
2427
+ history.trackError = error;
2428
+ history.assetLocationInfo = {
2429
+ ...refundInfo,
2430
+ type: "refund",
2431
+ };
2432
+ history.trackDone = true;
2433
+ });
2434
+ },
2435
+ onFulfill: () => {
2436
+ runInAction(() => {
2437
+ history.trackDone = true;
2438
+ });
2439
+ onFulfill();
2440
+ },
2441
+ });
2442
+ }
2443
+
2444
+ getRecentSwapV2History(id: string): SwapV2History | undefined {
2445
+ return this.recentSwapV2HistoryMap.get(id);
2446
+ }
2447
+
2448
+ getRecentSwapV2Histories(): SwapV2History[] {
2449
+ return Array.from(this.recentSwapV2HistoryMap.values()).filter(
2450
+ (history) => {
2451
+ if (!this.chainsService.hasChainInfo(history.fromChainId)) {
2452
+ return false;
2453
+ }
2454
+
2455
+ if (!this.chainsService.hasChainInfo(history.toChainId)) {
2456
+ return false;
2457
+ }
2458
+
2459
+ if (
2460
+ history.simpleRoute.some((route) => {
2461
+ return !this.chainsService.hasChainInfo(route.chainId);
2462
+ })
2463
+ ) {
2464
+ return false;
2465
+ }
2466
+
2467
+ return true;
2468
+ }
2469
+ );
2470
+ }
2471
+
2472
+ @action
2473
+ removeRecentSwapV2History(id: string): boolean {
2474
+ const history = this.getRecentSwapV2History(id);
2475
+ const removed = this.recentSwapV2HistoryMap.delete(id);
2476
+
2477
+ if (removed && history?.backgroundExecutionId) {
2478
+ this.publisher.publish({
2479
+ type: "remove",
2480
+ executionId: history.backgroundExecutionId,
2481
+ });
2482
+ }
2483
+
2484
+ return removed;
2485
+ }
2486
+
2487
+ @action
2488
+ clearAllRecentSwapV2History(): void {
2489
+ const executionIds: string[] = [];
2490
+ for (const history of this.recentSwapV2HistoryMap.values()) {
2491
+ if (history.backgroundExecutionId) {
2492
+ executionIds.push(history.backgroundExecutionId);
2493
+ }
2494
+ }
2495
+
2496
+ this.recentSwapV2HistoryMap.clear();
2497
+
2498
+ for (const executionId of executionIds) {
2499
+ this.publisher.publish({
2500
+ type: "remove",
2501
+ executionId,
2502
+ });
2503
+ }
2504
+ }
2505
+
2506
+ protected notifySwapV2Success(history: SwapV2History) {
2507
+ const notificationInfo = history.notificationInfo;
2508
+ if (!notificationInfo || history.notified) {
2509
+ return;
2510
+ }
2511
+
2512
+ const latestResAmount =
2513
+ history.resAmount.length > 0
2514
+ ? history.resAmount[history.resAmount.length - 1]
2515
+ : undefined;
2516
+ if (!latestResAmount || latestResAmount.length === 0) {
2517
+ return;
2518
+ }
2519
+
2520
+ const chainInfo = this.chainsService.getChainInfo(history.toChainId);
2521
+ if (!chainInfo) {
2522
+ return;
2523
+ }
2524
+
2525
+ const assetsText = latestResAmount
2526
+ .map((amt) => {
2527
+ const currency = notificationInfo.currencies.find(
2528
+ (cur) => cur.coinMinimalDenom === amt.denom
2529
+ );
2530
+ if (!currency) {
2531
+ return undefined;
2532
+ }
2533
+ return new CoinPretty(currency, amt.amount)
2534
+ .hideIBCMetadata(true)
2535
+ .shrink(true)
2536
+ .maxDecimals(6)
2537
+ .inequalitySymbol(true)
2538
+ .trim(true)
2539
+ .toString();
2540
+ })
2541
+ .filter((text): text is string => Boolean(text));
2542
+
2543
+ if (assetsText.length === 0) {
2544
+ return;
2545
+ }
2546
+
2547
+ runInAction(() => {
2548
+ history.notified = true;
2549
+ });
2550
+
2551
+ this.notification.create({
2552
+ iconRelativeUrl: "assets/logo-256.png",
2553
+ title: "Swap Succeeded",
2554
+ message: `${assetsText.join(", ")} received on ${chainInfo.chainName}`,
2555
+ });
2556
+ }
2557
+
2558
+ @action
2559
+ hideSwapV2History(id: string): boolean {
2560
+ const history = this.getRecentSwapV2History(id);
2561
+ if (!history) {
2562
+ return false;
2563
+ }
2564
+
2565
+ if (!history.backgroundExecutionId) {
2566
+ return false;
2567
+ }
2568
+
2569
+ // only hide if multi tx case
2570
+ history.hidden = true;
2571
+ return true;
2572
+ }
2573
+
2574
+ @action
2575
+ showSwapV2History(id: string): boolean {
2576
+ const history = this.getRecentSwapV2History(id);
2577
+ if (!history) {
2578
+ return false;
2579
+ }
2580
+ history.hidden = false;
2581
+ return true;
2582
+ }
2583
+
2584
+ @action
2585
+ setSwapV2HistoryError(id: string, error: string): boolean {
2586
+ const history = this.getRecentSwapV2History(id);
2587
+ if (!history) {
2588
+ return false;
2589
+ }
2590
+
2591
+ history.trackError = error;
2592
+ history.trackDone = true;
2593
+ return true;
2594
+ }
2595
+
2596
+ @action
2597
+ clearSwapV2HistoryBackgroundExecutionId(id: string): boolean {
2598
+ const history = this.getRecentSwapV2History(id);
2599
+ if (!history) {
2600
+ return false;
2601
+ }
2602
+
2603
+ history.backgroundExecutionId = undefined;
2604
+ return true;
2605
+ }
2606
+
2607
+ @action
2608
+ setSwapV2AdditionalTrackingData(
2609
+ id: string,
2610
+ data:
2611
+ | { type: "evm"; chainId: string; txHash: string }
2612
+ | {
2613
+ type: "cosmos-ibc";
2614
+ ibcSwapData: IBCSwapMinimalTrackingData;
2615
+ txHash: string;
2616
+ }
2617
+ ): boolean {
2618
+ const history = this.getRecentSwapV2History(id);
2619
+ if (!history) {
2620
+ return false;
2621
+ }
2622
+
2623
+ if (data.type === "cosmos-ibc") {
2624
+ history.additionalTrackingData = {
2625
+ type: "cosmos-ibc",
2626
+ chainId: data.ibcSwapData.chainId,
2627
+ swapReceiver: data.ibcSwapData.swapReceiver,
2628
+ swapChannelIndex: data.ibcSwapData.swapChannelIndex,
2629
+ txHash: data.txHash,
2630
+ txFulfilled: false,
2631
+ packetTimeout: false,
2632
+ ibcHistory: data.ibcSwapData.ibcChannels.map((ch) => ({
2633
+ portId: ch.portId,
2634
+ channelId: ch.channelId,
2635
+ counterpartyChainId: ch.counterpartyChainId,
2636
+ completed: false,
2637
+ })),
2638
+ };
2639
+ } else {
2640
+ history.additionalTrackingData = data;
2641
+ }
2642
+
2643
+ history.additionalTrackDone = false;
2644
+ history.additionalTrackError = undefined;
2645
+
2646
+ this.trackSwapV2AdditionalRecursive(id);
2647
+
2648
+ return true;
2649
+ }
2650
+
2651
+ trackSwapV2AdditionalRecursive(id: string): void {
2652
+ const history = this.getRecentSwapV2History(id);
2653
+ if (!history) {
2654
+ return;
2655
+ }
2656
+
2657
+ // no additional tracking data
2658
+ if (!history.additionalTrackingData) {
2659
+ return;
2660
+ }
2661
+
2662
+ // already done
2663
+ if (history.additionalTrackDone) {
2664
+ return;
2665
+ }
2666
+
2667
+ if (history.additionalTrackingData.type === "evm") {
2668
+ this.trackSwapV2AdditionalEVM(id);
2669
+ } else if (history.additionalTrackingData.type === "cosmos-ibc") {
2670
+ this.trackIBCPacketForwardingRecursive((onFulfill, onClose, onError) => {
2671
+ this.trackSwapV2AdditionalCosmosIBCInternal(
2672
+ id,
2673
+ onFulfill,
2674
+ onClose,
2675
+ onError
2676
+ );
2677
+ });
2678
+ }
2679
+ }
2680
+
2681
+ protected trackSwapV2AdditionalEVM(id: string): void {
2682
+ const history = this.getRecentSwapV2History(id);
2683
+ if (!history) {
2684
+ return;
2685
+ }
2686
+
2687
+ if (history.additionalTrackingData?.type !== "evm") {
2688
+ return;
2689
+ }
2690
+
2691
+ const txHash = history.additionalTrackingData.txHash;
2692
+
2693
+ this.traceEVMTransactionResult({
2694
+ chainId: history.toChainId,
2695
+ txHash,
2696
+ recipient: history.recipient,
2697
+ targetDenom: history.destinationAsset.denom,
2698
+ onResult: (result) => {
2699
+ runInAction(() => {
2700
+ if (result.success && result.resAmount) {
2701
+ history.resAmount.push(result.resAmount);
2702
+ history.assetLocationInfo = undefined;
2703
+ this.notifySwapV2Success(history);
2704
+ } else if (result.refundInfo) {
2705
+ history.additionalTrackError = result.error;
2706
+ history.assetLocationInfo = {
2707
+ ...result.refundInfo,
2708
+ type: "refund",
2709
+ };
2710
+ }
2711
+ history.additionalTrackDone = true;
2712
+ });
2713
+ },
2714
+ onFulfill: () => {
2715
+ runInAction(() => {
2716
+ history.additionalTrackDone = true;
2717
+ });
2718
+ },
2719
+ });
2720
+ }
2721
+
2722
+ protected trackSwapV2AdditionalCosmosIBCInternal(
2723
+ id: string,
2724
+ onFulfill: () => void,
2725
+ onClose: () => void,
2726
+ onError: () => void
2727
+ ): void {
2728
+ const history = this.getRecentSwapV2History(id);
2729
+ if (!history) {
2730
+ onFulfill();
2731
+ return;
2732
+ }
2733
+
2734
+ if (history.additionalTrackDone) {
2735
+ onFulfill();
2736
+ return;
2737
+ }
2738
+
2739
+ const existingTrackingData = history.additionalTrackingData;
2740
+ if (!existingTrackingData || existingTrackingData.type !== "cosmos-ibc") {
2741
+ onFulfill();
2742
+ return;
2743
+ }
2744
+
2745
+ const trackingData = existingTrackingData;
2746
+
2747
+ const { chainId, txHash, ibcHistory, swapReceiver, txFulfilled } =
2748
+ trackingData;
2749
+
2750
+ if (!txFulfilled) {
2751
+ this.trackIBCTxFulfillment({
2752
+ chainId,
2753
+ txHash,
2754
+ ibcHistory,
2755
+ swapReceiver,
2756
+ onTxFulfilled: (_tx, firstHopResAmount) => {
2757
+ runInAction(() => {
2758
+ trackingData.txFulfilled = true;
2759
+
2760
+ if (firstHopResAmount) {
2761
+ history.resAmount.push(firstHopResAmount);
2762
+ for (
2763
+ let i = history.routeIndex;
2764
+ i < history.simpleRoute.length;
2765
+ i++
2766
+ ) {
2767
+ const route = history.simpleRoute[i];
2768
+ if (route.chainId === chainId) {
2769
+ history.routeIndex = i + 1; // move to next route
2770
+ break;
2771
+ }
2772
+ }
2773
+ }
2774
+ });
2775
+ this.trackIBCPacketForwardingRecursive(
2776
+ (onFulfill, onClose, onError) => {
2777
+ this.trackSwapV2AdditionalCosmosIBCInternal(
2778
+ id,
2779
+ onFulfill,
2780
+ onClose,
2781
+ onError
2782
+ );
2783
+ }
2784
+ );
2785
+ },
2786
+ onTxError: (error) => {
2787
+ runInAction(() => {
2788
+ history.additionalTrackError = error;
2789
+ history.additionalTrackDone = true;
2790
+ });
2791
+ },
2792
+ onFulfill,
2793
+ onClose,
2794
+ onError,
2795
+ });
2796
+ return;
2797
+ }
2798
+
2799
+ if (
2800
+ this.handleIbcRewindIfNeeded({
2801
+ sourceChainId: chainId,
2802
+ ibcHistory,
2803
+ packetTimeout: trackingData.packetTimeout,
2804
+ swapContext:
2805
+ history.additionalTrackingData?.type === "cosmos-ibc"
2806
+ ? {
2807
+ swapReceiver,
2808
+ swapChannelIndex:
2809
+ history.additionalTrackingData.swapChannelIndex,
2810
+ setSwapRefundInfo: (refundInfo) => {
2811
+ runInAction(() => {
2812
+ history.assetLocationInfo = {
2813
+ ...refundInfo,
2814
+ type: "refund",
2815
+ };
2816
+ });
2817
+ },
2818
+ }
2819
+ : undefined,
2820
+ onFulfill,
2821
+ onClose,
2822
+ onError,
2823
+ onRewindComplete: () => {
2824
+ // rewound되었다면 실패 처리하는 것이 맞겠지...
2825
+ runInAction(() => {
2826
+ history.status = SwapV2TxStatus.FAILED;
2827
+ history.additionalTrackDone = true;
2828
+
2829
+ const rewoundChainId = ibcHistory.find(
2830
+ (h) => h.rewound
2831
+ )?.counterpartyChainId;
2832
+ if (rewoundChainId) {
2833
+ for (let i = history.simpleRoute.length - 1; i >= 0; i--) {
2834
+ const route = history.simpleRoute[i];
2835
+ if (route.chainId === rewoundChainId) {
2836
+ history.routeIndex = i; // 실패한 채널 인덱스로 이동
2837
+ break;
2838
+ }
2839
+ }
2840
+ }
2841
+ });
2842
+
2843
+ this.trackIBCPacketForwardingRecursive(
2844
+ (onFulfill, onClose, onError) => {
2845
+ this.trackSwapV2AdditionalCosmosIBCInternal(
2846
+ id,
2847
+ onFulfill,
2848
+ onClose,
2849
+ onError
2850
+ );
2851
+ }
2852
+ );
2853
+ },
2854
+ })
2855
+ ) {
2856
+ return;
2857
+ }
2858
+
2859
+ this.trackIbcHopFlowWithTimeout({
2860
+ ibcHistory,
2861
+ sourceChainId: chainId,
2862
+ swapReceiver,
2863
+ destinationAsset: history.destinationAsset,
2864
+ onHopCompleted: (resAmount) => {
2865
+ runInAction(() => {
2866
+ if (resAmount) {
2867
+ history.resAmount.push(resAmount);
2868
+ }
2869
+ });
2870
+ },
2871
+ onAllCompleted: () => {
2872
+ runInAction(() => {
2873
+ history.additionalTrackDone = true;
2874
+ history.assetLocationInfo = undefined;
2875
+ // Update routeIndex to the last index to show completion state in UI
2876
+ history.routeIndex = history.simpleRoute.length;
2877
+ this.notifySwapV2Success(history);
2878
+ });
2879
+ },
2880
+ onContinue: () => {
2881
+ this.trackIBCPacketForwardingRecursive(
2882
+ (onFulfill, onClose, onError) => {
2883
+ this.trackSwapV2AdditionalCosmosIBCInternal(
2884
+ id,
2885
+ onFulfill,
2886
+ onClose,
2887
+ onError
2888
+ );
2889
+ }
2890
+ );
2891
+ },
2892
+ onRetry: () => {
2893
+ this.trackIBCPacketForwardingRecursive(
2894
+ (onFulfill, onClose, onError) => {
2895
+ this.trackSwapV2AdditionalCosmosIBCInternal(
2896
+ id,
2897
+ onFulfill,
2898
+ onClose,
2899
+ onError
2900
+ );
2901
+ }
2902
+ );
2903
+ },
2904
+ onPacketTimeout: () => {
2905
+ runInAction(() => {
2906
+ trackingData.packetTimeout = true;
2907
+ });
2908
+ },
2909
+ onDynamicHopDetected: () => {
2910
+ runInAction(() => {
2911
+ trackingData.dynamicHopDetected = true;
2912
+ });
2913
+ },
2914
+ onFulfill,
2915
+ onClose,
2916
+ onError,
2917
+ });
2918
+ }
2919
+
2920
+ // ============================================================================
2921
+ // IBC Packet Tracking Core Functions
2922
+ // ============================================================================
2923
+
2924
+ /**
2925
+ * IBC tx 완료 대기 및 첫 번째 hop res amount 추출
2926
+ */
2927
+ protected trackIBCTxFulfillment(params: {
2928
+ chainId: string;
2929
+ txHash: string;
2930
+ ibcHistory: IbcHop[];
2931
+ swapReceiver?: string[];
2932
+ onTxFulfilled: (
2933
+ tx: any,
2934
+ firstHopResAmount?: { amount: string; denom: string }[]
2935
+ ) => void;
2936
+ onTxError: (error: string) => void;
2937
+ onFulfill: () => void;
2938
+ onClose: () => void;
2939
+ onError: () => void;
2940
+ }): void {
2941
+ const {
2942
+ chainId,
2943
+ txHash,
2944
+ ibcHistory,
2945
+ swapReceiver,
2946
+ onTxFulfilled,
2947
+ onTxError,
2948
+ onFulfill,
2949
+ onClose,
2950
+ onError,
2951
+ } = params;
2952
+
2953
+ const chainInfo = this.chainsService.getChainInfo(chainId);
2954
+ if (!chainInfo) {
2955
+ onFulfill();
2956
+ return;
2957
+ }
2958
+
2959
+ const txTracer = new TendermintTxTracer(chainInfo.rpc, "/websocket");
2960
+ txTracer.addEventListener("close", onClose);
2961
+ txTracer.addEventListener("error", onError);
2962
+
2963
+ txTracer.traceTx(Buffer.from(txHash, "hex")).then((tx) => {
2964
+ txTracer.close();
2965
+
2966
+ if (tx.code != null && tx.code !== 0) {
2967
+ onTxError(tx.log || tx.raw_log || "Tx failed");
2968
+ onFulfill();
2969
+ return;
2970
+ }
2971
+
2972
+ let resAmount: { amount: string; denom: string }[] | undefined;
2973
+
2974
+ if (swapReceiver && swapReceiver.length > 0) {
2975
+ resAmount = this.getIBCSwapResAmountFromTx(tx, swapReceiver[0]);
2976
+ }
2977
+
2978
+ if (ibcHistory.length > 0) {
2979
+ const firstChannel = ibcHistory[0];
2980
+
2981
+ firstChannel.sequence = this.getIBCPacketSequenceFromTx(
2982
+ tx,
2983
+ firstChannel.portId,
2984
+ firstChannel.channelId
2985
+ );
2986
+ firstChannel.dstChannelId = this.getDstChannelIdFromTx(
2987
+ tx,
2988
+ firstChannel.portId,
2989
+ firstChannel.channelId
2990
+ );
2991
+ }
2992
+ onTxFulfilled(tx, resAmount);
2993
+ onFulfill();
2994
+ });
2995
+ }
2996
+
2997
+ /**
2998
+ * IBC rewind 필요 여부 확인 및 rewind 처리
2999
+ */
3000
+ protected handleIbcRewindIfNeeded(params: {
3001
+ sourceChainId: string;
3002
+ ibcHistory: IbcHop[];
3003
+ packetTimeout?: boolean;
3004
+ swapContext?: {
3005
+ swapReceiver: string[];
3006
+ swapChannelIndex: number;
3007
+ setSwapRefundInfo?: (refundInfo: {
3008
+ chainId: string;
3009
+ amount: { amount: string; denom: string }[];
3010
+ }) => void;
3011
+ };
3012
+ onFulfill: () => void;
3013
+ onClose: () => void;
3014
+ onError: () => void;
3015
+ onRewindComplete: () => void;
3016
+ }): boolean {
3017
+ const {
3018
+ sourceChainId,
3019
+ ibcHistory,
3020
+ packetTimeout,
3021
+ swapContext,
3022
+ onFulfill,
3023
+ onClose,
3024
+ onError,
3025
+ onRewindComplete,
3026
+ } = params;
3027
+
3028
+ if (ibcHistory.length === 0) {
3029
+ return false;
3030
+ }
3031
+
3032
+ const needRewind = ibcHistory.find((h) => h.error != null) != null;
3033
+ if (!needRewind) {
3034
+ return false;
3035
+ }
3036
+
3037
+ if (ibcHistory.find((h) => h.rewoundButNextRewindingBlocked)) {
3038
+ onFulfill();
3039
+ return true;
3040
+ }
3041
+
3042
+ const isTimeoutPacket = packetTimeout ?? false;
3043
+ const lastRewoundChannelIndex = ibcHistory.findIndex((h) => {
3044
+ if (h.rewound) {
3045
+ return true;
3046
+ }
3047
+ });
3048
+ const targetChannel = (() => {
3049
+ if (lastRewoundChannelIndex >= 0) {
3050
+ if (lastRewoundChannelIndex === 0) {
3051
+ return undefined;
3052
+ }
3053
+
3054
+ return ibcHistory[lastRewoundChannelIndex - 1];
3055
+ }
3056
+ return ibcHistory.find((h) => h.error != null);
3057
+ })();
3058
+ const isSwapTargetChannel = !!(
3059
+ targetChannel &&
3060
+ swapContext &&
3061
+ ibcHistory.indexOf(targetChannel) === swapContext.swapChannelIndex + 1
3062
+ );
3063
+ if (targetChannel !== undefined && targetChannel.sequence !== undefined) {
3064
+ const targetSequence = targetChannel.sequence;
3065
+ const prevChainInfo = (() => {
3066
+ const targetChannelIndex = ibcHistory.findIndex(
3067
+ (h) => h === targetChannel
3068
+ );
3069
+ if (targetChannelIndex < 0) {
3070
+ return undefined;
3071
+ }
3072
+ if (targetChannelIndex === 0) {
3073
+ return this.chainsService.getChainInfo(sourceChainId);
3074
+ }
3075
+ return this.chainsService.getChainInfo(
3076
+ ibcHistory[targetChannelIndex - 1].counterpartyChainId
3077
+ );
3078
+ })();
3079
+
3080
+ if (prevChainInfo) {
3081
+ const txTracer = new TendermintTxTracer(
3082
+ prevChainInfo.rpc,
3083
+ "/websocket"
3084
+ );
3085
+ txTracer.addEventListener("close", onClose);
3086
+ txTracer.addEventListener("error", onError);
3087
+ txTracer
3088
+ .traceTx(
3089
+ isTimeoutPacket
3090
+ ? {
3091
+ // "timeout_packet.packet_src_port": targetChannel.portId,
3092
+ "timeout_packet.packet_src_channel": targetChannel.channelId,
3093
+ "timeout_packet.packet_sequence": targetChannel.sequence,
3094
+ }
3095
+ : {
3096
+ // "acknowledge_packet.packet_src_port": targetChannel.portId,
3097
+ "acknowledge_packet.packet_src_channel":
3098
+ targetChannel.channelId,
3099
+ "acknowledge_packet.packet_sequence": targetChannel.sequence,
3100
+ }
3101
+ )
3102
+ .then((res: any) => {
3103
+ txTracer.close();
3104
+
3105
+ if (!res) {
3106
+ return;
3107
+ }
3108
+
3109
+ runInAction(() => {
3110
+ if (isSwapTargetChannel) {
3111
+ const txs = res.txs
3112
+ ? res.txs.map((res: any) => res.tx_result || res)
3113
+ : [res.tx_result || res];
3114
+ if (txs && Array.isArray(txs)) {
3115
+ for (const tx of txs) {
3116
+ const index = isTimeoutPacket
3117
+ ? this.getIBCTimeoutPacketIndexFromTx(
3118
+ tx,
3119
+ targetChannel.portId,
3120
+ targetChannel.channelId,
3121
+ targetSequence
3122
+ )
3123
+ : this.getIBCAcknowledgementPacketIndexFromTx(
3124
+ tx,
3125
+ targetChannel.portId,
3126
+ targetChannel.channelId,
3127
+ targetSequence
3128
+ );
3129
+ if (index >= 0) {
3130
+ // 좀 빡치게 timeout packet은 refund 로직이 실행되고 나서 "timeout_packet" event가 발생한다.
3131
+ const refunded = isTimeoutPacket
3132
+ ? this.getIBCSwapResAmountFromTx(
3133
+ tx,
3134
+ swapContext.swapReceiver[
3135
+ swapContext.swapChannelIndex + 1
3136
+ ],
3137
+ (() => {
3138
+ const i =
3139
+ this.getLastIBCTimeoutPacketBeforeIndexFromTx(
3140
+ tx,
3141
+ index
3142
+ );
3143
+
3144
+ if (i < 0) {
3145
+ return 0;
3146
+ }
3147
+ return i;
3148
+ })(),
3149
+ index
3150
+ )
3151
+ : this.getIBCSwapResAmountFromTx(
3152
+ tx,
3153
+ swapContext.swapReceiver[
3154
+ swapContext.swapChannelIndex + 1
3155
+ ],
3156
+ index
3157
+ );
3158
+
3159
+ swapContext.setSwapRefundInfo?.({
3160
+ chainId: prevChainInfo.chainId,
3161
+ amount: refunded,
3162
+ });
3163
+
3164
+ targetChannel.rewoundButNextRewindingBlocked = true;
3165
+ break;
3166
+ }
3167
+ }
3168
+ }
3169
+ }
3170
+ targetChannel.rewound = true;
3171
+ });
3172
+ onFulfill();
3173
+ onRewindComplete();
3174
+ });
3175
+ }
3176
+ }
3177
+ return true;
3178
+ }
3179
+
3180
+ protected trackIbcHopFlowWithTimeout(params: {
3181
+ ibcHistory: IbcHop[];
3182
+ sourceChainId: string;
3183
+ swapReceiver?: string[];
3184
+ destinationAsset?: { chainId: string; denom: string };
3185
+ onHopCompleted?: (
3186
+ resAmount?: { amount: string; denom: string }[],
3187
+ tx?: any
3188
+ ) => void;
3189
+ onAllCompleted: () => void;
3190
+ onContinue: () => void;
3191
+ onRetry: () => void;
3192
+ onPacketTimeout?: () => void;
3193
+ onDynamicHopDetected?: () => void;
3194
+ onFulfill: () => void;
3195
+ onClose: () => void;
3196
+ onError: () => void;
3197
+ }): void {
3198
+ const {
3199
+ ibcHistory,
3200
+ sourceChainId,
3201
+ swapReceiver,
3202
+ destinationAsset,
3203
+ onHopCompleted,
3204
+ onAllCompleted,
3205
+ onContinue,
3206
+ onRetry,
3207
+ onPacketTimeout,
3208
+ onDynamicHopDetected,
3209
+ onFulfill,
3210
+ onClose,
3211
+ onError,
3212
+ } = params;
3213
+
3214
+ const targetChannelIndex = ibcHistory.findIndex((history) => {
3215
+ return !history.completed;
3216
+ });
3217
+
3218
+ const targetChannel = ibcHistory[targetChannelIndex];
3219
+ if (!targetChannel || !targetChannel.sequence) {
3220
+ onFulfill();
3221
+ return;
3222
+ }
3223
+
3224
+ const closables: {
3225
+ readyState: WsReadyState;
3226
+ close: () => void;
3227
+ }[] = [];
3228
+
3229
+ const closeClosables = () => {
3230
+ closables.forEach((closable) => {
3231
+ if (
3232
+ closable.readyState === WsReadyState.OPEN ||
3233
+ closable.readyState === WsReadyState.CONNECTING
3234
+ ) {
3235
+ closable.close();
3236
+ }
3237
+ });
3238
+ };
3239
+
3240
+ let _onFulfillOnce = false;
3241
+ const onFulfillOnce = () => {
3242
+ if (!_onFulfillOnce) {
3243
+ _onFulfillOnce = true;
3244
+ closeClosables();
3245
+ onFulfill();
3246
+ }
3247
+ };
3248
+
3249
+ let _onCloseOnce = false;
3250
+ const onCloseOnce = () => {
3251
+ if (!_onCloseOnce) {
3252
+ _onCloseOnce = true;
3253
+ closeClosables();
3254
+ onClose();
3255
+ }
3256
+ };
3257
+
3258
+ let _onErrorOnce = false;
3259
+ const onErrorOnce = () => {
3260
+ if (!_onErrorOnce) {
3261
+ _onErrorOnce = true;
3262
+ closeClosables();
3263
+ onError();
3264
+ }
3265
+ };
3266
+
3267
+ const registerClosable = (closable: {
3268
+ readyState: WsReadyState;
3269
+ close: () => void;
3270
+ }) => closables.push(closable);
3271
+
3272
+ let hopFailed = false;
3273
+
3274
+ const hopTracer = this.trackIBCHopRecvPacket({
3275
+ ibcHistory,
3276
+ targetChannelIndex,
3277
+ swapReceiver,
3278
+ destinationAsset,
3279
+ onHopCompleted: (resAmount, tx) => {
3280
+ onHopCompleted?.(resAmount, tx);
3281
+
3282
+ const sequence = targetChannel.sequence;
3283
+ if (!sequence) {
3284
+ hopFailed = true;
3285
+ onFulfillOnce();
3286
+ return;
3287
+ }
3288
+
3289
+ if (!tx) {
3290
+ return;
3291
+ }
3292
+
3293
+ const txs = tx.txs
3294
+ ? tx.txs.map((res: any) => res.tx_result || res)
3295
+ : [tx.tx_result || tx];
3296
+
3297
+ for (const txItem of txs) {
3298
+ try {
3299
+ const ack = this.getIBCWriteAcknowledgementAckFromTx(
3300
+ txItem,
3301
+ targetChannel.portId,
3302
+ targetChannel.channelId,
3303
+ sequence
3304
+ );
3305
+
3306
+ if (ack && ack.length > 0) {
3307
+ const str = Buffer.from(ack);
3308
+ try {
3309
+ const decoded = JSON.parse(str.toString());
3310
+ if (decoded.error) {
3311
+ runInAction(() => {
3312
+ targetChannel.error = "Packet processing failed";
3313
+ });
3314
+ hopFailed = true;
3315
+ onFulfillOnce();
3316
+ onRetry();
3317
+ return;
3318
+ }
3319
+ } catch (e) {
3320
+ console.log(e);
3321
+ }
3322
+ }
3323
+ } catch {
3324
+ // noop
3325
+ }
3326
+
3327
+ const index = this.getIBCRecvPacketIndexFromTx(
3328
+ txItem,
3329
+ targetChannel.portId,
3330
+ targetChannel.channelId,
3331
+ sequence
3332
+ );
3333
+
3334
+ if (index >= 0) {
3335
+ break;
3336
+ }
3337
+ }
3338
+ },
3339
+ onAllCompleted: () => {
3340
+ if (hopFailed) {
3341
+ return;
3342
+ }
3343
+ onAllCompleted();
3344
+ onFulfillOnce();
3345
+ },
3346
+ onContinue: () => {
3347
+ if (hopFailed) {
3348
+ return;
3349
+ }
3350
+ onContinue();
3351
+ onFulfillOnce();
3352
+ },
3353
+ onFulfill: onFulfillOnce,
3354
+ onClose: onCloseOnce,
3355
+ onError: onErrorOnce,
3356
+ onDynamicHopDetected: (hopInfo) => {
3357
+ if (hopFailed) {
3358
+ return;
3359
+ }
3360
+
3361
+ // 동적으로 감지된 새 홉을 ibcHistory에 추가한다.
3362
+ runInAction(() => {
3363
+ ibcHistory.push({
3364
+ portId: hopInfo.portId,
3365
+ channelId: hopInfo.channelId,
3366
+ counterpartyChainId: hopInfo.counterpartyChainId,
3367
+ sequence: hopInfo.sequence,
3368
+ dstChannelId: hopInfo.dstChannelId,
3369
+ completed: false,
3370
+ });
3371
+
3372
+ if (swapReceiver && hopInfo.packetData.receiver) {
3373
+ swapReceiver.push(hopInfo.packetData.receiver);
3374
+ }
3375
+ });
3376
+
3377
+ onDynamicHopDetected?.();
3378
+ onContinue();
3379
+ onFulfillOnce();
3380
+ },
3381
+ });
3382
+
3383
+ if (hopTracer) {
3384
+ registerClosable(hopTracer);
3385
+ }
3386
+
3387
+ let prevChainId: string | undefined;
3388
+ if (targetChannelIndex > 0) {
3389
+ prevChainId = ibcHistory[targetChannelIndex - 1].counterpartyChainId;
3390
+ } else {
3391
+ prevChainId = sourceChainId;
3392
+ }
3393
+ if (prevChainId) {
3394
+ const prevChainInfo = this.chainsService.getChainInfo(prevChainId);
3395
+ if (prevChainInfo) {
3396
+ const queryEvents: any = {
3397
+ // acknowledge_packet과는 다르게 timeout_packet은 이전의 체인의 이벤트로부터만 알 수 있다.
3398
+ // 방법이 없기 때문에 여기서 이전의 체인으로부터 subscribe를 해서 이벤트를 받아야 한다.
3399
+ // 하지만 이 경우 ibc error tracking 로직에서 이것과 똑같은 subscription을 한번 더 하게 된다.
3400
+ // 이미 로직이 많이 복잡하기 때문에 로직을 덜 복잡하게 하기 위해서 이러한 비효율성(?)을 감수한다.
3401
+ // "timeout_packet.packet_src_port": targetChannel.portId,
3402
+ "timeout_packet.packet_src_channel": targetChannel.channelId,
3403
+ "timeout_packet.packet_sequence": targetChannel.sequence,
3404
+ };
3405
+
3406
+ const txTracer = new TendermintTxTracer(
3407
+ prevChainInfo.rpc,
3408
+ "/websocket"
3409
+ );
3410
+ registerClosable(txTracer);
3411
+ txTracer.addEventListener("close", onCloseOnce);
3412
+ txTracer.addEventListener("error", onErrorOnce);
3413
+ txTracer.traceTx(queryEvents).then((res) => {
3414
+ txTracer.close();
1775
3415
 
1776
- if (receiveTxHash) {
1777
- this.trackDestinationAssetAmount(id, receiveTxHash, onFulfill);
1778
- } else {
1779
- history.trackDone = true;
1780
- onFulfill();
1781
- }
1782
- break;
3416
+ if (!res) {
3417
+ return;
3418
+ }
1783
3419
 
1784
- case "STATE_PENDING":
1785
- case "STATE_PENDING_ERROR":
1786
- // 아직 트래킹 중이거나 에러 상태 전파 중 => 재시도
1787
- onError();
1788
- break;
1789
- }
1790
- })
1791
- .catch((e) => {
1792
- console.error(e);
1793
- onError();
1794
- });
1795
- };
3420
+ runInAction(() => {
3421
+ targetChannel.error = "Packet timeout";
3422
+ onPacketTimeout?.();
3423
+ hopFailed = true;
3424
+ onFulfillOnce();
3425
+ onRetry();
3426
+ });
3427
+ });
3428
+ }
3429
+ }
3430
+ }
1796
3431
 
1797
- protected trackDestinationAssetAmount(
1798
- historyId: string,
1799
- txHash: string,
1800
- onFulfill: () => void
1801
- ) {
1802
- const history = this.getRecentSkipHistory(historyId);
1803
- if (!history) {
1804
- onFulfill();
3432
+ protected trackIBCHopRecvPacket(params: {
3433
+ ibcHistory: IbcHop[];
3434
+ targetChannelIndex: number;
3435
+ swapReceiver?: string[];
3436
+ destinationAsset?: { chainId: string; denom: string };
3437
+ onHopCompleted: (
3438
+ resAmount?: { amount: string; denom: string }[],
3439
+ tx?: any
3440
+ ) => void;
3441
+ onAllCompleted: () => void;
3442
+ onContinue: () => void;
3443
+ onFulfill: () => void;
3444
+ onClose: () => void;
3445
+ onError: () => void;
3446
+ onDynamicHopDetected?: (hopInfo: {
3447
+ portId: string;
3448
+ channelId: string;
3449
+ counterpartyChainId: string;
3450
+ sequence: string;
3451
+ dstChannelId: string;
3452
+ packetData: { denom: string; amount: string; receiver: string };
3453
+ }) => void;
3454
+ }): TendermintTxTracer | undefined {
3455
+ const {
3456
+ ibcHistory,
3457
+ targetChannelIndex,
3458
+ swapReceiver,
3459
+ destinationAsset,
3460
+ onHopCompleted,
3461
+ onAllCompleted,
3462
+ onContinue,
3463
+ onFulfill,
3464
+ onClose,
3465
+ onError,
3466
+ onDynamicHopDetected,
3467
+ } = params;
3468
+
3469
+ const targetChannel = ibcHistory[targetChannelIndex];
3470
+ const nextChannel =
3471
+ targetChannelIndex + 1 < ibcHistory.length
3472
+ ? ibcHistory[targetChannelIndex + 1]
3473
+ : undefined;
3474
+
3475
+ if (!targetChannel || !targetChannel.sequence) {
3476
+ onAllCompleted();
1805
3477
  return;
1806
3478
  }
3479
+ const sequence = targetChannel.sequence;
1807
3480
 
1808
- const chainInfo = this.chainsService.getChainInfo(
1809
- history.destinationChainId
3481
+ const counterpartyChainInfo = this.chainsService.getChainInfo(
3482
+ targetChannel.counterpartyChainId
1810
3483
  );
1811
- if (!chainInfo) {
3484
+
3485
+ if (!counterpartyChainInfo) {
1812
3486
  onFulfill();
1813
3487
  return;
1814
3488
  }
1815
3489
 
1816
- if (this.chainsService.isEvmChain(history.destinationChainId)) {
1817
- const evmInfo = chainInfo.evm;
1818
- if (!evmInfo) {
1819
- onFulfill();
3490
+ const queryEvents: any = {
3491
+ // "recv_packet.packet_src_port": targetChannel.portId,
3492
+ "recv_packet.packet_dst_channel": targetChannel.dstChannelId,
3493
+ "recv_packet.packet_sequence": targetChannel.sequence,
3494
+ };
3495
+
3496
+ const txTracer = new TendermintTxTracer(
3497
+ counterpartyChainInfo.rpc,
3498
+ "/websocket"
3499
+ );
3500
+ txTracer.addEventListener("close", onClose);
3501
+ txTracer.addEventListener("error", onError);
3502
+
3503
+ txTracer.traceTx(queryEvents).then((res) => {
3504
+ txTracer.close();
3505
+
3506
+ if (!res) {
3507
+ onError();
1820
3508
  return;
1821
3509
  }
1822
3510
 
1823
- simpleFetch<{
1824
- result: EthTxReceipt | null;
1825
- error?: Error;
1826
- }>(evmInfo.rpc, {
1827
- method: "POST",
1828
- headers: {
1829
- "content-type": "application/json",
1830
- "request-source": origin,
1831
- },
1832
- body: JSON.stringify({
1833
- jsonrpc: "2.0",
1834
- method: "eth_getTransactionReceipt",
1835
- params: [txHash],
1836
- id: 1,
1837
- }),
1838
- })
1839
- .then((res) => {
1840
- const txReceipt = res.data.result;
1841
- if (txReceipt) {
1842
- simpleFetch<{
1843
- result: any;
1844
- error?: Error;
1845
- }>(evmInfo.rpc, {
1846
- method: "POST",
1847
- headers: {
1848
- "content-type": "application/json",
1849
- "request-source": origin,
1850
- },
1851
- body: JSON.stringify({
1852
- jsonrpc: "2.0",
1853
- method: "debug_traceTransaction",
1854
- params: [txHash, { tracer: "callTracer" }],
1855
- id: 1,
1856
- }),
1857
- }).then((res) => {
1858
- runInAction(() => {
1859
- let isFoundFromCall = false;
1860
- if (res.data.result) {
1861
- const searchForTransfers = (calls: any) => {
1862
- for (const call of calls) {
1863
- if (
1864
- call.type === "CALL" &&
1865
- call.to.toLowerCase() ===
1866
- history.recipient.toLowerCase()
1867
- ) {
1868
- const isERC20Transfer =
1869
- call.input.startsWith("0xa9059cbb");
1870
- const value = BigInt(
1871
- isERC20Transfer
1872
- ? `0x${call.input.substring(74)}`
1873
- : call.value
1874
- );
3511
+ const txs = res.txs
3512
+ ? res.txs.map((t: any) => t.tx_result || t)
3513
+ : [res.tx_result || res];
1875
3514
 
1876
- history.resAmount.push([
1877
- {
1878
- amount: value.toString(10),
1879
- denom: history.destinationAsset.denom,
1880
- },
1881
- ]);
1882
- isFoundFromCall = true;
1883
- }
1884
-
1885
- if (call.calls && call.calls.length > 0) {
1886
- searchForTransfers(call.calls);
1887
- }
1888
- }
1889
- };
3515
+ const matchedTx = txs.find((tx: any) => {
3516
+ const idx = this.getIBCRecvPacketIndexFromTx(
3517
+ tx,
3518
+ targetChannel.portId,
3519
+ targetChannel.channelId,
3520
+ sequence
3521
+ );
3522
+ return idx >= 0;
3523
+ });
1890
3524
 
1891
- searchForTransfers(res.data.result.calls || []);
1892
- }
3525
+ const tx = matchedTx || txs[0];
1893
3526
 
1894
- if (isFoundFromCall) {
1895
- history.trackDone = true;
1896
- return;
1897
- }
3527
+ targetChannel.completed = true;
1898
3528
 
1899
- const logs = txReceipt.logs;
1900
- const transferTopic = id("Transfer(address,address,uint256)");
1901
- const withdrawTopic = id("Withdrawal(address,uint256)");
1902
- const hyperlaneReceiveTopic = id(
1903
- "ReceivedTransferRemote(uint32,bytes32,uint256)"
1904
- );
1905
- for (const log of logs) {
1906
- if (log.topics[0] === transferTopic) {
1907
- const to = "0x" + log.topics[2].slice(26);
1908
- if (to.toLowerCase() === history.recipient.toLowerCase()) {
1909
- const destinationAssetDenom =
1910
- history.destinationAsset.denom.replace("erc20:", "");
1911
-
1912
- const amount = BigInt(log.data).toString(10);
1913
- if (log.address === destinationAssetDenom) {
1914
- history.resAmount.push([
1915
- {
1916
- amount,
1917
- denom: history.destinationAsset.denom,
1918
- },
1919
- ]);
1920
- } else {
1921
- console.log("refunded", log.address);
1922
- // Transfer 토픽인 경우엔 ERC20의 tranfer 호출일텐데
1923
- // 받을 토큰의 컨트랙트가 아닌 다른 컨트랙트에서 호출된 경우는 Swap을 실패한 것으로 추측
1924
- // 고로 실제로 받은 토큰의 컨트랙트 주소로 환불 정보에 저장한다.
1925
- history.trackError = "Swap failed";
1926
- history.swapRefundInfo = {
1927
- chainId: history.destinationChainId,
1928
- amount: [
1929
- {
1930
- amount,
1931
- denom: `erc20:${log.address.toLowerCase()}`,
1932
- },
1933
- ],
1934
- };
1935
- }
1936
-
1937
- history.trackDone = true;
1938
- return;
1939
- }
1940
- } else if (log.topics[0] === withdrawTopic) {
1941
- const to = "0x" + log.topics[1].slice(26);
1942
- if (to.toLowerCase() === txReceipt.to.toLowerCase()) {
1943
- const amount = BigInt(log.data).toString(10);
1944
- history.resAmount.push([
1945
- { amount, denom: history.destinationAsset.denom },
1946
- ]);
1947
- history.trackDone = true;
1948
- return;
1949
- }
1950
- } else if (log.topics[0] === hyperlaneReceiveTopic) {
1951
- const to = "0x" + log.topics[2].slice(26);
1952
- if (to.toLowerCase() === history.recipient.toLowerCase()) {
1953
- const amount = BigInt(log.data).toString(10);
1954
- // Hyperlane을 통해 Forma로 TIA를 받는 경우 토큰 수량이 decimal 6으로 기록되는데,
1955
- // Forma에서는 decimal 18이기 때문에 12자리 만큼 0을 붙여준다.
1956
- history.resAmount.push([
1957
- {
1958
- amount:
1959
- history.destinationAsset.denom === "forma-native"
1960
- ? `${amount}000000000000`
1961
- : amount,
1962
- denom: history.destinationAsset.denom,
1963
- },
1964
- ]);
1965
- history.trackDone = true;
1966
- return;
1967
- }
1968
- }
1969
- }
1970
- });
1971
- });
1972
- }
1973
- })
1974
- .finally(() => {
1975
- history.trackDone = true;
1976
- onFulfill();
1977
- });
1978
- } else {
1979
- const txTracer = new TendermintTxTracer(chainInfo.rpc, "/websocket");
1980
- txTracer.addEventListener("error", () => onFulfill());
1981
- txTracer
1982
- .queryTx({
1983
- "tx.hash": txHash,
1984
- })
1985
- .then((res: any) => {
1986
- txTracer.close();
3529
+ let resAmount: { amount: string; denom: string }[] | undefined;
3530
+ const receiverIndex = targetChannelIndex + 1;
1987
3531
 
1988
- if (!res) {
1989
- return;
1990
- }
1991
- runInAction(() => {
1992
- const txs = res.txs
1993
- ? res.txs.map((res: any) => res.tx_result || res)
1994
- : [res.tx_result || res];
1995
- for (const tx of txs) {
1996
- const resAmount = this.getIBCSwapResAmountFromTx(
1997
- tx,
1998
- history.recipient
1999
- );
3532
+ if (tx && swapReceiver && receiverIndex < swapReceiver.length) {
3533
+ const index = this.getIBCRecvPacketIndexFromTx(
3534
+ tx,
3535
+ targetChannel.portId,
3536
+ targetChannel.channelId,
3537
+ sequence
3538
+ );
2000
3539
 
2001
- history.resAmount.push(resAmount);
2002
- history.trackDone = true;
3540
+ if (index >= 0) {
3541
+ resAmount = this.getIBCSwapResAmountFromTx(
3542
+ tx,
3543
+ swapReceiver[receiverIndex],
3544
+ index,
3545
+ undefined
3546
+ );
3547
+ }
3548
+ }
3549
+
3550
+ onHopCompleted(resAmount, tx);
3551
+
3552
+ // 1. 다음 채널이 있고, tx가 있는 경우 => 다음 채널로 이동
3553
+ // 2. 마지막 채널이면서, destinationAsset가 있고, onDynamicHopDetected가 있는 경우 => 동적 홉 감지
3554
+ // 3. 그 외의 경우 => 종료(allCompleted)
3555
+ if (nextChannel && tx) {
3556
+ const index = this.getIBCRecvPacketIndexFromTx(
3557
+ tx,
3558
+ targetChannel.portId,
3559
+ targetChannel.channelId,
3560
+ sequence
3561
+ );
3562
+
3563
+ if (index >= 0) {
3564
+ nextChannel.sequence = this.getIBCPacketSequenceFromTx(
3565
+ tx,
3566
+ nextChannel.portId,
3567
+ nextChannel.channelId,
3568
+ index
3569
+ );
3570
+ nextChannel.dstChannelId = this.getDstChannelIdFromTx(
3571
+ tx,
3572
+ nextChannel.portId,
3573
+ nextChannel.channelId,
3574
+ index
3575
+ );
3576
+ }
3577
+
3578
+ onContinue();
3579
+ } else if (tx && destinationAsset && onDynamicHopDetected) {
3580
+ // 마지막 채널에서 recv_packet 이후 새로운 send_packet이 있는 경우를 감지
3581
+ const index = this.getIBCRecvPacketIndexFromTx(
3582
+ tx,
3583
+ targetChannel.portId,
3584
+ targetChannel.channelId,
3585
+ sequence
3586
+ );
3587
+
3588
+ if (index >= 0) {
3589
+ const sendPackets = this.getSendPacketInfoFromTx(tx, index);
3590
+ if (sendPackets.length > 0) {
3591
+ // 마지막 send_packet을 확인
3592
+ const lastSendPacket = sendPackets[sendPackets.length - 1];
3593
+
3594
+ // destination chain에서 IBC wrapped token을 unwrap하는 경우를 가정한다.
3595
+ // 예를 들어, source chain -> osmosis (swap) -> destination chain으로 이동하는 경우,
3596
+ // osmosis에서 IBC wrapped token이 destination chain으로 전송되고,
3597
+ // destination chain에서 IBC wrapped token을 unwrap하는 케이스가 있을 수 있다.
3598
+ const destinationChainInfo = this.chainsService.getChainInfo(
3599
+ destinationAsset.chainId
3600
+ );
3601
+
3602
+ if (destinationChainInfo) {
3603
+ onDynamicHopDetected({
3604
+ portId: "transfer", // CHECK: transfer를 하드코딩해도 되는지 확인
3605
+ channelId: lastSendPacket.srcChannel,
3606
+ counterpartyChainId: destinationChainInfo.chainId,
3607
+ sequence: lastSendPacket.sequence,
3608
+ dstChannelId: lastSendPacket.dstChannel,
3609
+ packetData: lastSendPacket.packetData,
3610
+ });
2003
3611
  return;
2004
3612
  }
2005
- });
2006
- })
2007
- .finally(() => {
2008
- history.trackDone = true;
2009
- onFulfill();
2010
- });
2011
- }
2012
- }
3613
+ }
3614
+ }
2013
3615
 
2014
- @action
2015
- removeRecentSkipHistory(id: string): boolean {
2016
- return this.recentSkipHistoryMap.delete(id);
2017
- }
3616
+ onAllCompleted();
3617
+ } else {
3618
+ onAllCompleted();
3619
+ }
3620
+ });
2018
3621
 
2019
- @action
2020
- clearAllRecentSkipHistory(): void {
2021
- this.recentSkipHistoryMap.clear();
3622
+ return txTracer;
2022
3623
  }
2023
3624
 
2024
3625
  protected getIBCWriteAcknowledgementAckFromTx(
@@ -2197,6 +3798,7 @@ export class RecentSendHistoryService {
2197
3798
  if (split.length === 5) {
2198
3799
  const amount = split[1];
2199
3800
  const denom = split[3];
3801
+
2200
3802
  return [
2201
3803
  {
2202
3804
  denom,
@@ -2210,6 +3812,124 @@ export class RecentSendHistoryService {
2210
3812
  return [];
2211
3813
  }
2212
3814
 
3815
+ /**
3816
+ * TX에서 send_packet 이벤트 정보를 추출하여 추가 IBC 홉을 감지하기 위해 사용됩니다.
3817
+ *
3818
+ * 예를 들어, 마지막 목적지 체인에서 IBC wrapped token을 unwrap하는 경우,
3819
+ * send_packet 이벤트를 통해 추가 IBC 홉을 감지할 수 있습니다.
3820
+ */
3821
+ protected getSendPacketInfoFromTx(
3822
+ tx: any,
3823
+ startEventsIndex: number = 0
3824
+ ): {
3825
+ srcChannel: string;
3826
+ dstChannel: string;
3827
+ sequence: string;
3828
+ packetData: { denom: string; amount: string; receiver: string };
3829
+ }[] {
3830
+ const events = tx.events;
3831
+ if (!events || !Array.isArray(events)) {
3832
+ return [];
3833
+ }
3834
+
3835
+ const compareStringWithBase64OrPlain = (
3836
+ target: string,
3837
+ value: string
3838
+ ): [boolean, boolean] => {
3839
+ if (target === value) {
3840
+ return [true, false];
3841
+ }
3842
+ if (target === Buffer.from(value).toString("base64")) {
3843
+ return [true, true];
3844
+ }
3845
+ return [false, false];
3846
+ };
3847
+
3848
+ const results: {
3849
+ srcChannel: string;
3850
+ dstChannel: string;
3851
+ sequence: string;
3852
+ packetData: { denom: string; amount: string; receiver: string };
3853
+ }[] = [];
3854
+
3855
+ const slicedEvents = events.slice(startEventsIndex);
3856
+
3857
+ for (const event of slicedEvents) {
3858
+ if (event.type !== "send_packet") {
3859
+ continue;
3860
+ }
3861
+
3862
+ let isBase64 = false;
3863
+
3864
+ const srcChannelAttr = event.attributes?.find((attr: { key: string }) => {
3865
+ const c = compareStringWithBase64OrPlain(
3866
+ attr.key,
3867
+ "packet_src_channel"
3868
+ );
3869
+ isBase64 = c[1];
3870
+ return c[0];
3871
+ });
3872
+ if (!srcChannelAttr) {
3873
+ continue;
3874
+ }
3875
+
3876
+ const dstChannelAttr = event.attributes?.find((attr: { key: string }) => {
3877
+ return compareStringWithBase64OrPlain(
3878
+ attr.key,
3879
+ "packet_dst_channel"
3880
+ )[0];
3881
+ });
3882
+ if (!dstChannelAttr) continue;
3883
+
3884
+ const sequenceAttr = event.attributes?.find((attr: { key: string }) => {
3885
+ return compareStringWithBase64OrPlain(attr.key, "packet_sequence")[0];
3886
+ });
3887
+ if (!sequenceAttr) continue;
3888
+
3889
+ const packetDataAttr = event.attributes?.find((attr: { key: string }) => {
3890
+ return compareStringWithBase64OrPlain(attr.key, "packet_data")[0];
3891
+ });
3892
+ if (!packetDataAttr) continue;
3893
+
3894
+ try {
3895
+ const srcChannel = isBase64
3896
+ ? Buffer.from(srcChannelAttr.value, "base64").toString()
3897
+ : srcChannelAttr.value;
3898
+ const dstChannel = isBase64
3899
+ ? Buffer.from(dstChannelAttr.value, "base64").toString()
3900
+ : dstChannelAttr.value;
3901
+ const sequence = isBase64
3902
+ ? Buffer.from(sequenceAttr.value, "base64").toString()
3903
+ : sequenceAttr.value;
3904
+
3905
+ const packetDataValue = isBase64
3906
+ ? Buffer.from(packetDataAttr.value, "base64").toString()
3907
+ : packetDataAttr.value.startsWith("{")
3908
+ ? packetDataAttr.value
3909
+ : Buffer.from(packetDataAttr.value, "base64").toString();
3910
+
3911
+ const packetData = JSON.parse(packetDataValue);
3912
+
3913
+ if (packetData.denom && packetData.amount && packetData.receiver) {
3914
+ results.push({
3915
+ srcChannel,
3916
+ dstChannel,
3917
+ sequence,
3918
+ packetData: {
3919
+ denom: packetData.denom,
3920
+ amount: packetData.amount,
3921
+ receiver: packetData.receiver,
3922
+ },
3923
+ });
3924
+ }
3925
+ } catch {
3926
+ // noop
3927
+ }
3928
+ }
3929
+
3930
+ return results;
3931
+ }
3932
+
2213
3933
  protected getIBCAcknowledgementPacketIndexFromTx(
2214
3934
  tx: any,
2215
3935
  sourcePortId: string,
@@ -2684,31 +4404,172 @@ export class RecentSendHistoryService {
2684
4404
  throw new Error("Invalid tx");
2685
4405
  }
2686
4406
 
4407
+ // ============================================================================
4408
+ // Chain removed handler
4409
+ // ============================================================================
2687
4410
  protected readonly onChainRemoved = (chainInfo: ChainInfo) => {
2688
4411
  const chainIdentifier = ChainIdHelper.parse(chainInfo.chainId).identifier;
4412
+ try {
4413
+ this.removeIBCHistoriesByChainIdentifier(chainIdentifier);
4414
+ this.removeSkipHistoriesByChainIdentifier(chainIdentifier);
4415
+ this.removeSwapV2HistoriesByChainIdentifier(chainIdentifier);
4416
+ } catch (e) {
4417
+ console.error(e);
4418
+ }
4419
+ };
2689
4420
 
2690
- runInAction(() => {
2691
- const removingIds: string[] = [];
2692
- for (const history of this.recentIBCHistoryMap.values()) {
4421
+ @action
4422
+ protected removeIBCHistoriesByChainIdentifier(chainIdentifier: string): void {
4423
+ const removingIds: string[] = [];
4424
+ for (const history of this.recentIBCHistoryMap.values()) {
4425
+ if (ChainIdHelper.parse(history.chainId).identifier === chainIdentifier) {
4426
+ removingIds.push(history.id);
4427
+ continue;
4428
+ }
4429
+
4430
+ if (
4431
+ ChainIdHelper.parse(history.destinationChainId).identifier ===
4432
+ chainIdentifier
4433
+ ) {
4434
+ removingIds.push(history.id);
4435
+ continue;
4436
+ }
4437
+
4438
+ if (
4439
+ history.ibcHistory.some((history) => {
4440
+ return (
4441
+ ChainIdHelper.parse(history.counterpartyChainId).identifier ===
4442
+ chainIdentifier
4443
+ );
4444
+ })
4445
+ ) {
4446
+ removingIds.push(history.id);
4447
+ continue;
4448
+ }
4449
+ }
4450
+
4451
+ for (const id of removingIds) {
4452
+ this.recentIBCHistoryMap.delete(id);
4453
+ }
4454
+ }
4455
+
4456
+ @action
4457
+ protected removeSkipHistoriesByChainIdentifier(
4458
+ chainIdentifier: string
4459
+ ): void {
4460
+ const removingIds: string[] = [];
4461
+ for (const history of this.recentSkipHistoryMap.values()) {
4462
+ if (ChainIdHelper.parse(history.chainId).identifier === chainIdentifier) {
4463
+ removingIds.push(history.id);
4464
+ continue;
4465
+ }
4466
+
4467
+ if (
4468
+ ChainIdHelper.parse(history.destinationChainId).identifier ===
4469
+ chainIdentifier
4470
+ ) {
4471
+ removingIds.push(history.id);
4472
+ continue;
4473
+ }
4474
+
4475
+ if (
4476
+ ChainIdHelper.parse(history.destinationAsset.chainId).identifier ===
4477
+ chainIdentifier
4478
+ ) {
4479
+ removingIds.push(history.id);
4480
+ continue;
4481
+ }
4482
+
4483
+ if (
4484
+ history.simpleRoute.some(
4485
+ (route) =>
4486
+ ChainIdHelper.parse(route.chainId).identifier === chainIdentifier
4487
+ )
4488
+ ) {
4489
+ removingIds.push(history.id);
4490
+ continue;
4491
+ }
4492
+
4493
+ if (
4494
+ history.swapRefundInfo &&
4495
+ ChainIdHelper.parse(history.swapRefundInfo.chainId).identifier ===
4496
+ chainIdentifier
4497
+ ) {
4498
+ removingIds.push(history.id);
4499
+ continue;
4500
+ }
4501
+
4502
+ if (history.transferAssetRelease) {
4503
+ const chainId = history.transferAssetRelease.chain_id;
4504
+ const isOnlyEvm = parseInt(chainId) > 0;
4505
+ const chainIdInKeplr = isOnlyEvm ? `eip155:${chainId}` : chainId;
2693
4506
  if (
2694
- ChainIdHelper.parse(history.chainId).identifier === chainIdentifier
4507
+ ChainIdHelper.parse(chainIdInKeplr).identifier === chainIdentifier
2695
4508
  ) {
2696
4509
  removingIds.push(history.id);
2697
4510
  continue;
2698
4511
  }
4512
+ }
4513
+ }
2699
4514
 
2700
- if (
2701
- ChainIdHelper.parse(history.destinationChainId).identifier ===
4515
+ for (const id of removingIds) {
4516
+ this.recentSkipHistoryMap.delete(id);
4517
+ }
4518
+ }
4519
+
4520
+ @action
4521
+ protected removeSwapV2HistoriesByChainIdentifier(
4522
+ chainIdentifier: string
4523
+ ): void {
4524
+ const removingIds: string[] = [];
4525
+ for (const history of this.recentSwapV2HistoryMap.values()) {
4526
+ if (
4527
+ ChainIdHelper.parse(history.fromChainId).identifier === chainIdentifier
4528
+ ) {
4529
+ removingIds.push(history.id);
4530
+ continue;
4531
+ }
4532
+
4533
+ if (
4534
+ ChainIdHelper.parse(history.toChainId).identifier === chainIdentifier
4535
+ ) {
4536
+ removingIds.push(history.id);
4537
+ continue;
4538
+ }
4539
+
4540
+ if (
4541
+ history.simpleRoute.some(
4542
+ (route) =>
4543
+ ChainIdHelper.parse(route.chainId).identifier === chainIdentifier
4544
+ )
4545
+ ) {
4546
+ removingIds.push(history.id);
4547
+ continue;
4548
+ }
4549
+
4550
+ if (
4551
+ history.assetLocationInfo &&
4552
+ ChainIdHelper.parse(history.assetLocationInfo.chainId).identifier ===
2702
4553
  chainIdentifier
4554
+ ) {
4555
+ removingIds.push(history.id);
4556
+ continue;
4557
+ }
4558
+
4559
+ if (history.additionalTrackingData) {
4560
+ if (
4561
+ ChainIdHelper.parse(history.additionalTrackingData.chainId)
4562
+ .identifier === chainIdentifier
2703
4563
  ) {
2704
4564
  removingIds.push(history.id);
2705
4565
  continue;
2706
4566
  }
2707
4567
 
2708
4568
  if (
2709
- history.ibcHistory.some((history) => {
4569
+ history.additionalTrackingData.type === "cosmos-ibc" &&
4570
+ history.additionalTrackingData.ibcHistory.some((h) => {
2710
4571
  return (
2711
- ChainIdHelper.parse(history.counterpartyChainId).identifier ===
4572
+ ChainIdHelper.parse(h.counterpartyChainId).identifier ===
2712
4573
  chainIdentifier
2713
4574
  );
2714
4575
  })
@@ -2717,10 +4578,38 @@ export class RecentSendHistoryService {
2717
4578
  continue;
2718
4579
  }
2719
4580
  }
4581
+ }
2720
4582
 
2721
- for (const id of removingIds) {
2722
- this.recentIBCHistoryMap.delete(id);
2723
- }
2724
- });
2725
- };
4583
+ for (const id of removingIds) {
4584
+ this.recentSwapV2HistoryMap.delete(id);
4585
+ }
4586
+ }
4587
+
4588
+ // ============================================================================
4589
+ // Helper Functions
4590
+ // ============================================================================
4591
+
4592
+ private getExecutableChainIdsFromSwapV2History(
4593
+ history: SwapV2History,
4594
+ includeAllChainIds: boolean = false
4595
+ ): string[] {
4596
+ const chainIds: string[] = [];
4597
+
4598
+ const endIndex = includeAllChainIds
4599
+ ? history.simpleRoute.length
4600
+ : Math.max(0, history.routeIndex + 1);
4601
+
4602
+ for (let i = 0; i < endIndex; i++) {
4603
+ chainIds.push(history.simpleRoute[i].chainId);
4604
+ }
4605
+
4606
+ if (
4607
+ history.assetLocationInfo &&
4608
+ history.assetLocationInfo.type === "intermediate"
4609
+ ) {
4610
+ chainIds.push(history.assetLocationInfo.chainId);
4611
+ }
4612
+
4613
+ return chainIds;
4614
+ }
2726
4615
  }