@keplr-wallet/background 0.12.313 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -16,22 +16,26 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
16
16
  };
17
17
  var _a;
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.RecentSendHistoryService = void 0;
19
+ exports.RecentSendHistoryService = exports.UNKNOWN_TX_STATUS_TIMEOUT_MS = void 0;
20
20
  const cosmos_1 = require("@keplr-wallet/cosmos");
21
21
  const mobx_1 = require("mobx");
22
22
  const common_1 = require("@keplr-wallet/common");
23
+ const types_1 = require("./types");
23
24
  const buffer_1 = require("buffer/");
24
- const types_1 = require("@keplr-wallet/types");
25
+ const types_2 = require("@keplr-wallet/types");
25
26
  const unit_1 = require("@keplr-wallet/unit");
26
- const simple_fetch_1 = require("@keplr-wallet/simple-fetch");
27
27
  const hash_1 = require("@ethersproject/hash");
28
+ const api_1 = require("./api");
29
+ exports.UNKNOWN_TX_STATUS_TIMEOUT_MS = 5 * 60 * 1000; // 5분
28
30
  const SWAP_API_ENDPOINT = (_a = process.env["KEPLR_API_ENDPOINT"]) !== null && _a !== void 0 ? _a : "";
29
31
  class RecentSendHistoryService {
30
- constructor(kvStore, chainsService, txService, notification) {
32
+ constructor(kvStore, chainsService, txService, analyticsService, notification, publisher) {
31
33
  this.kvStore = kvStore;
32
34
  this.chainsService = chainsService;
33
35
  this.txService = txService;
36
+ this.analyticsService = analyticsService;
34
37
  this.notification = notification;
38
+ this.publisher = publisher;
35
39
  // Key: {chain_identifier}/{type}
36
40
  this.recentSendHistoryMap = new Map();
37
41
  this.recentIBCHistorySeq = 0;
@@ -40,6 +44,9 @@ class RecentSendHistoryService {
40
44
  this.recentSkipHistorySeq = 0;
41
45
  // Key: id (sequence, it should be increased by 1 for each)
42
46
  this.recentSkipHistoryMap = new Map();
47
+ this.recentSwapV2HistorySeq = 0;
48
+ // Key: id (sequence, it should be increased by 1 for each)
49
+ this.recentSwapV2HistoryMap = new Map();
43
50
  // ibc packet forwarding을 위한 recursive function이면서
44
51
  // 실패시 retry를 수행하기 위해서 분리되어 있음
45
52
  // trackIBCPacketForwardingRecursive도 참고
@@ -51,505 +58,178 @@ class RecentSendHistoryService {
51
58
  onFulfill();
52
59
  return;
53
60
  }
54
- const needRewind = (() => {
55
- if (!history.txFulfilled) {
56
- return false;
57
- }
58
- if (history.ibcHistory.length === 0) {
59
- return false;
60
- }
61
- return history.ibcHistory.find((h) => h.error != null) != null;
62
- })();
63
- if (needRewind) {
64
- if (history.ibcHistory.find((h) => h.rewoundButNextRewindingBlocked)) {
65
- onFulfill();
66
- return;
67
- }
68
- const isTimeoutPacket = history.packetTimeout || false;
69
- const lastRewoundChannelIndex = history.ibcHistory.findIndex((h) => {
70
- if (h.rewound) {
71
- return true;
72
- }
73
- });
74
- const targetChannel = (() => {
75
- if (lastRewoundChannelIndex >= 0) {
76
- if (lastRewoundChannelIndex === 0) {
77
- return undefined;
78
- }
79
- return history.ibcHistory[lastRewoundChannelIndex - 1];
80
- }
81
- return history.ibcHistory.find((h) => h.error != null);
82
- })();
83
- const isSwapTargetChannel = targetChannel &&
84
- "swapChannelIndex" in history &&
85
- history.ibcHistory.indexOf(targetChannel) ===
86
- history.swapChannelIndex + 1;
87
- if (targetChannel && targetChannel.sequence) {
88
- const prevChainInfo = (() => {
89
- const targetChannelIndex = history.ibcHistory.findIndex((h) => h === targetChannel);
90
- if (targetChannelIndex < 0) {
91
- return undefined;
92
- }
93
- if (targetChannelIndex === 0) {
94
- return this.chainsService.getChainInfo(history.chainId);
95
- }
96
- return this.chainsService.getChainInfo(history.ibcHistory[targetChannelIndex - 1].counterpartyChainId);
97
- })();
98
- if (prevChainInfo) {
99
- const txTracer = new cosmos_1.TendermintTxTracer(prevChainInfo.rpc, "/websocket");
100
- txTracer.addEventListener("close", onClose);
101
- txTracer.addEventListener("error", onError);
102
- txTracer
103
- .traceTx(isTimeoutPacket
104
- ? {
105
- // "timeout_packet.packet_src_port": targetChannel.portId,
106
- "timeout_packet.packet_src_channel": targetChannel.channelId,
107
- "timeout_packet.packet_sequence": targetChannel.sequence,
108
- }
109
- : {
110
- // "acknowledge_packet.packet_src_port": targetChannel.portId,
111
- "acknowledge_packet.packet_src_channel": targetChannel.channelId,
112
- "acknowledge_packet.packet_sequence": targetChannel.sequence,
113
- })
114
- .then((res) => {
115
- txTracer.close();
116
- if (!res) {
117
- return;
118
- }
119
- (0, mobx_1.runInAction)(() => {
120
- if (isSwapTargetChannel) {
121
- const txs = res.txs
122
- ? res.txs.map((res) => res.tx_result || res)
123
- : [res.tx_result || res];
124
- if (txs && Array.isArray(txs)) {
125
- for (const tx of txs) {
126
- if (targetChannel.sequence && "swapReceiver" in history) {
127
- const index = isTimeoutPacket
128
- ? this.getIBCTimeoutPacketIndexFromTx(tx, targetChannel.portId, targetChannel.channelId, targetChannel.sequence)
129
- : this.getIBCAcknowledgementPacketIndexFromTx(tx, targetChannel.portId, targetChannel.channelId, targetChannel.sequence);
130
- if (index >= 0) {
131
- // 좀 빡치게 timeout packet은 refund 로직이 실행되고 나서 "timeout_packet" event가 발생한다.
132
- const refunded = isTimeoutPacket
133
- ? this.getIBCSwapResAmountFromTx(tx, history.swapReceiver[history.swapChannelIndex + 1], (() => {
134
- const i = this.getLastIBCTimeoutPacketBeforeIndexFromTx(tx, index);
135
- if (i < 0) {
136
- return 0;
137
- }
138
- return i;
139
- })(), index)
140
- : this.getIBCSwapResAmountFromTx(tx, history.swapReceiver[history.swapChannelIndex + 1], index);
141
- history.swapRefundInfo = {
142
- chainId: prevChainInfo.chainId,
143
- amount: refunded,
144
- };
145
- targetChannel.rewoundButNextRewindingBlocked = true;
146
- break;
147
- }
148
- }
149
- }
150
- }
151
- }
152
- targetChannel.rewound = true;
153
- });
154
- onFulfill();
155
- this.trackIBCPacketForwardingRecursive(id);
156
- });
157
- }
158
- }
159
- }
160
- else if (!history.txFulfilled) {
161
- const chainId = history.chainId;
162
- const chainInfo = this.chainsService.getChainInfo(chainId);
163
- const txHash = buffer_1.Buffer.from(history.txHash, "hex");
164
- if (chainInfo) {
165
- const txTracer = new cosmos_1.TendermintTxTracer(chainInfo.rpc, "/websocket");
166
- txTracer.addEventListener("close", onClose);
167
- txTracer.addEventListener("error", onError);
168
- txTracer.traceTx(txHash).then((tx) => {
169
- txTracer.close();
61
+ if (!history.txFulfilled) {
62
+ this.trackIBCTxFulfillment({
63
+ chainId: history.chainId,
64
+ txHash: history.txHash,
65
+ ibcHistory: history.ibcHistory,
66
+ swapReceiver: "swapReceiver" in history ? history.swapReceiver : undefined,
67
+ onTxFulfilled: (_tx, firstHopResAmount) => {
170
68
  (0, mobx_1.runInAction)(() => {
171
69
  history.txFulfilled = true;
172
- if (tx.code != null && tx.code !== 0) {
173
- history.txError = tx.log || tx.raw_log || "Unknown error";
174
- // TODO: In this case, it is not currently displayed in the UI. So, delete it for now.
175
- // 어차피 tx 자체의 실패는 notification으로 알 수 있기 때문에 여기서 지우더라도 유저는 실패를 인지할 수 있다.
176
- this.removeRecentIBCHistory(id);
177
- }
178
- else {
179
- if ("swapReceiver" in history) {
180
- const resAmount = this.getIBCSwapResAmountFromTx(tx, history.swapReceiver[0]);
181
- history.resAmount.push(resAmount);
182
- }
183
- if (history.ibcHistory.length > 0) {
184
- const firstChannel = history.ibcHistory[0];
185
- firstChannel.sequence = this.getIBCPacketSequenceFromTx(tx, firstChannel.portId, firstChannel.channelId);
186
- firstChannel.dstChannelId = this.getDstChannelIdFromTx(tx, firstChannel.portId, firstChannel.channelId);
187
- onFulfill();
188
- this.trackIBCPacketForwardingRecursive(id);
189
- }
70
+ if ("swapReceiver" in history && firstHopResAmount) {
71
+ history.resAmount.push(firstHopResAmount);
190
72
  }
191
73
  });
192
- });
193
- }
194
- }
195
- else if (history.ibcHistory.length > 0) {
196
- const targetChannelIndex = history.ibcHistory.findIndex((history) => {
197
- return !history.completed;
74
+ this.trackIBCPacketForwardingRecursive((onFulfill, onClose, onError) => {
75
+ this.trackIBCPacketForwardingRecursiveInternal(id, onFulfill, onClose, onError);
76
+ });
77
+ },
78
+ onTxError: () => {
79
+ this.removeRecentIBCHistory(id);
80
+ },
81
+ onFulfill: onFulfill,
82
+ onClose: onClose,
83
+ onError: onError,
198
84
  });
199
- const targetChannel = targetChannelIndex >= 0
200
- ? history.ibcHistory[targetChannelIndex]
201
- : undefined;
202
- const nextChannel = targetChannelIndex >= 0 &&
203
- targetChannelIndex + 1 < history.ibcHistory.length
204
- ? history.ibcHistory[targetChannelIndex + 1]
205
- : undefined;
206
- if (targetChannel && targetChannel.sequence) {
207
- const closables = [];
208
- let _onFulfillOnce = false;
209
- const onFulfillOnce = () => {
210
- if (!_onFulfillOnce) {
211
- _onFulfillOnce = true;
212
- closables.forEach((closable) => {
213
- if (closable.readyState === cosmos_1.WsReadyState.OPEN ||
214
- closable.readyState === cosmos_1.WsReadyState.CONNECTING) {
215
- closable.close();
216
- }
217
- });
218
- onFulfill();
219
- }
220
- };
221
- let _onCloseOnce = false;
222
- const onCloseOnce = () => {
223
- if (!_onCloseOnce) {
224
- _onCloseOnce = true;
225
- closables.forEach((closable) => {
226
- if (closable.readyState === cosmos_1.WsReadyState.OPEN ||
227
- closable.readyState === cosmos_1.WsReadyState.CONNECTING) {
228
- closable.close();
229
- }
230
- });
231
- onClose();
232
- }
233
- };
234
- let _onErrorOnce = false;
235
- const onErrorOnce = () => {
236
- if (!_onErrorOnce) {
237
- _onErrorOnce = true;
238
- closables.forEach((closable) => {
239
- if (closable.readyState === cosmos_1.WsReadyState.OPEN ||
240
- closable.readyState === cosmos_1.WsReadyState.CONNECTING) {
241
- closable.close();
242
- }
85
+ return;
86
+ }
87
+ if (this.handleIbcRewindIfNeeded({
88
+ sourceChainId: history.chainId,
89
+ ibcHistory: history.ibcHistory,
90
+ packetTimeout: history.packetTimeout,
91
+ swapContext: "swapReceiver" in history && "swapChannelIndex" in history
92
+ ? {
93
+ swapReceiver: history.swapReceiver,
94
+ swapChannelIndex: history.swapChannelIndex,
95
+ setSwapRefundInfo: (refundInfo) => {
96
+ (0, mobx_1.runInAction)(() => {
97
+ history.swapRefundInfo = refundInfo;
243
98
  });
244
- onError();
99
+ },
100
+ }
101
+ : undefined,
102
+ onFulfill,
103
+ onClose,
104
+ onError,
105
+ onRewindComplete: () => {
106
+ this.trackIBCPacketForwardingRecursive((onFulfill, onClose, onError) => {
107
+ this.trackIBCPacketForwardingRecursiveInternal(id, onFulfill, onClose, onError);
108
+ });
109
+ },
110
+ })) {
111
+ return;
112
+ }
113
+ this.trackIbcHopFlowWithTimeout({
114
+ ibcHistory: history.ibcHistory,
115
+ sourceChainId: history.chainId,
116
+ swapReceiver: "swapReceiver" in history ? history.swapReceiver : undefined,
117
+ onHopCompleted: (resAmount) => {
118
+ (0, mobx_1.runInAction)(() => {
119
+ if (resAmount && "resAmount" in history) {
120
+ history.resAmount.push(resAmount);
245
121
  }
246
- };
247
- const chainInfo = this.chainsService.getChainInfo(targetChannel.counterpartyChainId);
248
- if (chainInfo) {
249
- const queryEvents = {
250
- // "recv_packet.packet_src_port": targetChannel.portId,
251
- "recv_packet.packet_dst_channel": targetChannel.dstChannelId,
252
- "recv_packet.packet_sequence": targetChannel.sequence,
253
- };
254
- const txTracer = new cosmos_1.TendermintTxTracer(chainInfo.rpc, "/websocket");
255
- closables.push(txTracer);
256
- txTracer.addEventListener("close", onCloseOnce);
257
- txTracer.addEventListener("error", onErrorOnce);
258
- txTracer.traceTx(queryEvents).then((res) => {
259
- txTracer.close();
260
- if (!res) {
261
- return;
262
- }
263
- const txs = res.txs
264
- ? res.txs.map((res) => res.tx_result || res)
265
- : [res.tx_result || res];
266
- if (txs && Array.isArray(txs)) {
267
- (0, mobx_1.runInAction)(() => {
268
- targetChannel.completed = true;
269
- for (const tx of txs) {
270
- try {
271
- const ack = this.getIBCWriteAcknowledgementAckFromTx(tx, targetChannel.portId, targetChannel.channelId, targetChannel.sequence);
272
- if (ack && ack.length > 0) {
273
- const str = buffer_1.Buffer.from(ack);
274
- try {
275
- const decoded = JSON.parse(str.toString());
276
- if (decoded.error) {
277
- // XXX: {key: 'packet_ack', value: '{"error":"ABCI code: 6: error handling packet: see events for details"}'}
278
- // 오류가 있을 경우 이딴식으로 오류가 나오기 때문에 뭐 유저에게 보여줄 방법이 없다...
279
- targetChannel.error = "Packet processing failed";
280
- onFulfillOnce();
281
- this.trackIBCPacketForwardingRecursive(id);
282
- break;
283
- }
284
- }
285
- catch (e) {
286
- // decode가 실패한 경우 사실 방법이 없다.
287
- // 일단 packet이 성공했다고 치고 진행한다.
288
- console.log(e);
289
- }
290
- }
291
- // Because a tx can contain multiple messages, it's hard to know exactly which event we want.
292
- // But logically, the events closest to the recv_packet event is the events we want.
293
- const index = this.getIBCRecvPacketIndexFromTx(tx, targetChannel.portId, targetChannel.channelId, targetChannel.sequence);
294
- if (index >= 0) {
295
- if ("swapReceiver" in history) {
296
- const res = this.getIBCSwapResAmountFromTx(tx, history.swapReceiver[targetChannelIndex + 1], index);
297
- history.resAmount.push(res);
298
- }
299
- if (nextChannel) {
300
- nextChannel.sequence = this.getIBCPacketSequenceFromTx(tx, nextChannel.portId, nextChannel.channelId, index);
301
- nextChannel.dstChannelId = this.getDstChannelIdFromTx(tx, nextChannel.portId, nextChannel.channelId, index);
302
- onFulfillOnce();
303
- this.trackIBCPacketForwardingRecursive(id);
304
- break;
305
- }
306
- else {
307
- // Packet received to destination chain.
308
- if (history.notificationInfo && !history.notified) {
309
- (0, mobx_1.runInAction)(() => {
310
- history.notified = true;
311
- });
312
- const chainInfo = this.chainsService.getChainInfo(history.destinationChainId);
313
- if (chainInfo) {
314
- if ("swapType" in history) {
315
- if (history.resAmount.length > 0) {
316
- const amount = history.resAmount[history.resAmount.length - 1];
317
- const assetsText = amount
318
- .filter((amt) => history.notificationInfo.currencies.find((cur) => cur.coinMinimalDenom === amt.denom))
319
- .map((amt) => {
320
- const currency = history.notificationInfo.currencies.find((cur) => cur.coinMinimalDenom === amt.denom);
321
- return new unit_1.CoinPretty(currency, amt.amount)
322
- .hideIBCMetadata(true)
323
- .shrink(true)
324
- .maxDecimals(6)
325
- .inequalitySymbol(true)
326
- .trim(true)
327
- .toString();
328
- });
329
- if (assetsText.length > 0) {
330
- // Notify user
331
- this.notification.create({
332
- iconRelativeUrl: "assets/logo-256.png",
333
- title: "IBC Swap Succeeded",
334
- message: `${assetsText.join(", ")} received on ${chainInfo.chainName}`,
335
- });
336
- }
337
- }
338
- }
339
- else {
340
- const assetsText = history.amount
341
- .filter((amt) => history.notificationInfo.currencies.find((cur) => cur.coinMinimalDenom === amt.denom))
342
- .map((amt) => {
343
- const currency = history.notificationInfo.currencies.find((cur) => cur.coinMinimalDenom === amt.denom);
344
- return new unit_1.CoinPretty(currency, amt.amount)
345
- .hideIBCMetadata(true)
346
- .shrink(true)
347
- .maxDecimals(6)
348
- .inequalitySymbol(true)
349
- .trim(true)
350
- .toString();
351
- });
352
- if (assetsText.length > 0) {
353
- // Notify user
354
- this.notification.create({
355
- iconRelativeUrl: "assets/logo-256.png",
356
- title: "IBC Transfer Succeeded",
357
- message: `${assetsText.join(", ")} sent to ${chainInfo.chainName}`,
358
- });
359
- }
360
- }
361
- }
362
- }
363
- onFulfillOnce();
364
- break;
365
- }
366
- }
367
- }
368
- catch (_a) {
369
- // noop
122
+ });
123
+ },
124
+ onAllCompleted: () => {
125
+ const notificationInfo = history.notificationInfo;
126
+ if (notificationInfo && !history.notified) {
127
+ (0, mobx_1.runInAction)(() => {
128
+ history.notified = true;
129
+ });
130
+ const chainInfo = this.chainsService.getChainInfo(history.destinationChainId);
131
+ if (chainInfo) {
132
+ if ("swapType" in history) {
133
+ if (history.resAmount.length > 0) {
134
+ const amount = history.resAmount[history.resAmount.length - 1];
135
+ const assetsText = amount
136
+ .map((amt) => {
137
+ const currency = notificationInfo.currencies.find((cur) => cur.coinMinimalDenom === amt.denom);
138
+ if (!currency) {
139
+ return undefined;
370
140
  }
141
+ return new unit_1.CoinPretty(currency, amt.amount)
142
+ .hideIBCMetadata(true)
143
+ .shrink(true)
144
+ .maxDecimals(6)
145
+ .inequalitySymbol(true)
146
+ .trim(true)
147
+ .toString();
148
+ })
149
+ .filter((text) => Boolean(text));
150
+ if (assetsText.length > 0) {
151
+ this.notification.create({
152
+ iconRelativeUrl: "assets/logo-256.png",
153
+ title: "IBC Swap Succeeded",
154
+ message: `${assetsText.join(", ")} received on ${chainInfo.chainName}`,
155
+ });
371
156
  }
372
- });
157
+ }
373
158
  }
374
- });
375
- }
376
- let prevChainId = "";
377
- if (targetChannelIndex > 0) {
378
- prevChainId =
379
- history.ibcHistory[targetChannelIndex - 1].counterpartyChainId;
380
- }
381
- else {
382
- prevChainId = history.chainId;
383
- }
384
- if (prevChainId) {
385
- const prevChainInfo = this.chainsService.getChainInfo(prevChainId);
386
- if (prevChainInfo) {
387
- const queryEvents = {
388
- // acknowledge_packet과는 다르게 timeout_packet은 이전의 체인의 이벤트로부터만 알 수 있다.
389
- // 방법이 없기 때문에 여기서 이전의 체인으로부터 subscribe를 해서 이벤트를 받아야 한다.
390
- // 하지만 경우 ibc error tracking 로직에서 이것과 똑같은 subscription을 한번 더 하게 된다.
391
- // 이미 로직이 많이 복잡하기 때문에 로직을 덜 복잡하게 하기 위해서 이러한 비효율성(?)을 감수한다.
392
- // "timeout_packet.packet_src_port": targetChannel.portId,
393
- "timeout_packet.packet_src_channel": targetChannel.channelId,
394
- "timeout_packet.packet_sequence": targetChannel.sequence,
395
- };
396
- const txTracer = new cosmos_1.TendermintTxTracer(prevChainInfo.rpc, "/websocket");
397
- closables.push(txTracer);
398
- txTracer.addEventListener("close", onCloseOnce);
399
- txTracer.addEventListener("error", onErrorOnce);
400
- txTracer.traceTx(queryEvents).then((res) => {
401
- txTracer.close();
402
- if (!res) {
403
- return;
159
+ else {
160
+ const assetsText = history.amount
161
+ .map((amt) => {
162
+ const currency = notificationInfo.currencies.find((cur) => cur.coinMinimalDenom === amt.denom);
163
+ if (!currency) {
164
+ return undefined;
165
+ }
166
+ return new unit_1.CoinPretty(currency, amt.amount)
167
+ .hideIBCMetadata(true)
168
+ .shrink(true)
169
+ .maxDecimals(6)
170
+ .inequalitySymbol(true)
171
+ .trim(true)
172
+ .toString();
173
+ })
174
+ .filter((text) => Boolean(text));
175
+ if (assetsText.length > 0) {
176
+ this.notification.create({
177
+ iconRelativeUrl: "assets/logo-256.png",
178
+ title: "IBC Transfer Succeeded",
179
+ message: `${assetsText.join(", ")} sent to ${chainInfo.chainName}`,
180
+ });
404
181
  }
405
- // 이 event가 발생한 시점에서 이미 timeout packet은 받은 상태이고
406
- // 이 경우 따로 정보를 얻을 필요는 없으므로 이후에 res를 쓰지는 않는다.
407
- // 위에 res null check는 사실 필요 없지만 혹시나 해서 넣어둔다.
408
- (0, mobx_1.runInAction)(() => {
409
- targetChannel.error = "Packet timeout";
410
- history.packetTimeout = true;
411
- onFulfillOnce();
412
- this.trackIBCPacketForwardingRecursive(id);
413
- });
414
- });
182
+ }
415
183
  }
416
184
  }
417
- }
418
- }
185
+ },
186
+ onContinue: () => {
187
+ this.trackIBCPacketForwardingRecursive((onFulfill, onClose, onError) => {
188
+ this.trackIBCPacketForwardingRecursiveInternal(id, onFulfill, onClose, onError);
189
+ });
190
+ },
191
+ onRetry: () => {
192
+ this.trackIBCPacketForwardingRecursive((onFulfill, onClose, onError) => {
193
+ this.trackIBCPacketForwardingRecursiveInternal(id, onFulfill, onClose, onError);
194
+ });
195
+ },
196
+ onPacketTimeout: () => {
197
+ (0, mobx_1.runInAction)(() => {
198
+ history.packetTimeout = true;
199
+ });
200
+ },
201
+ onFulfill,
202
+ onClose,
203
+ onError,
204
+ });
419
205
  };
420
- this.checkAndTrackSkipSwapTxFulfilledRecursive = (history, onFulfill, onError) => {
421
- const chainInfo = this.chainsService.getChainInfo(history.chainId);
206
+ this.checkAndTrackSwapTxFulfilledRecursive = (params) => {
207
+ const { chainId, txHash, onSuccess, onPending, onFailed, onError } = params;
208
+ const chainInfo = this.chainsService.getChainInfo(chainId);
422
209
  if (!chainInfo) {
423
- onFulfill(false);
210
+ onFailed();
424
211
  return;
425
212
  }
426
- if (this.chainsService.isEvmChain(history.chainId)) {
427
- const evmInfo = chainInfo.evm;
428
- if (!evmInfo) {
429
- onFulfill(false);
430
- return;
431
- }
432
- (0, simple_fetch_1.simpleFetch)(evmInfo.rpc, {
433
- method: "POST",
434
- headers: {
435
- "content-type": "application/json",
436
- "request-source": origin,
437
- },
438
- body: JSON.stringify({
439
- jsonrpc: "2.0",
440
- method: "eth_getTransactionReceipt",
441
- params: [history.txHash],
442
- id: 1,
443
- }),
444
- })
445
- .then((res) => {
446
- const txReceipt = res.data.result;
447
- if (txReceipt) {
448
- if (txReceipt.status === types_1.EthTxStatus.Success) {
449
- setTimeout(() => {
450
- (0, simple_fetch_1.simpleFetch)(SWAP_API_ENDPOINT, "/v1/swap/tx", {
451
- method: "POST",
452
- headers: Object.assign({ "content-type": "application/json" }, (() => {
453
- const res = {};
454
- if (process.env["SKIP_API_KEY"]) {
455
- res.authorization = process.env["SKIP_API_KEY"];
456
- }
457
- return res;
458
- })()),
459
- body: JSON.stringify({
460
- tx_hash: history.txHash,
461
- chain_id: history.chainId.replace("eip155:", ""),
462
- }),
463
- })
464
- .then((result) => {
465
- console.log(`Skip tx track result: ${JSON.stringify(result)}`);
466
- onFulfill(true);
467
- })
468
- .catch((e) => {
469
- console.log(e);
470
- this.removeRecentSkipHistory(history.id);
471
- onFulfill(false);
472
- });
473
- }, 2000);
474
- }
475
- else {
476
- // tx가 실패한거면 종료
477
- this.removeRecentSkipHistory(history.id);
478
- onFulfill(false);
479
- }
480
- }
481
- else {
482
- onError();
483
- }
484
- })
485
- .catch(() => {
486
- // 오류가 발생하면 종료
487
- onFulfill(false);
488
- });
489
- }
490
- else {
491
- const txTracer = new cosmos_1.TendermintTxTracer(chainInfo.rpc, "/websocket");
492
- txTracer.addEventListener("error", () => onFulfill(false));
493
- txTracer
494
- .traceTx(buffer_1.Buffer.from(history.txHash.replace("0x", ""), "hex"))
495
- .then((res) => {
496
- txTracer.close();
497
- let txResult;
498
- if (Array.isArray(res.txs)) {
499
- if (res.txs && res.txs.length > 0) {
500
- txResult = res.txs[0].tx_result;
501
- }
502
- else {
503
- // In case tx is not confirmed, just wait for next check
504
- onError();
505
- return;
506
- }
507
- }
508
- else {
509
- txResult = res;
510
- }
511
- if (!txResult || typeof txResult.code !== "number") {
213
+ this.resolveTxExecutionStatus(chainInfo, chainId, txHash)
214
+ .then((status) => {
215
+ switch (status) {
216
+ case "success":
217
+ onSuccess();
218
+ break;
219
+ case "pending":
220
+ onPending();
221
+ break;
222
+ case "failed":
223
+ onFailed();
224
+ break;
225
+ default:
512
226
  onError();
513
- return;
514
- }
515
- if (txResult.code === 0) {
516
- setTimeout(() => {
517
- (0, simple_fetch_1.simpleFetch)(SWAP_API_ENDPOINT, "/v1/swap/tx", {
518
- method: "POST",
519
- headers: Object.assign({ "content-type": "application/json" }, (() => {
520
- const res = {};
521
- if (process.env["SKIP_API_KEY"]) {
522
- res.authorization = process.env["SKIP_API_KEY"];
523
- }
524
- return res;
525
- })()),
526
- body: JSON.stringify({
527
- tx_hash: history.txHash,
528
- chain_id: history.chainId,
529
- }),
530
- })
531
- .then((result) => {
532
- console.log(`Skip tx track result: ${JSON.stringify(result)}`);
533
- onFulfill(true);
534
- })
535
- .catch((e) => {
536
- console.log(e);
537
- this.removeRecentSkipHistory(history.id);
538
- onFulfill(false);
539
- });
540
- }, 2000);
541
- }
542
- else {
543
- // tx가 실패한거면 종료
544
- this.removeRecentSkipHistory(history.id);
545
- onFulfill(false);
546
- }
547
- })
548
- .catch(() => {
549
- // 오류가 발생하면 종료
550
- onFulfill(false);
551
- });
552
- }
227
+ break;
228
+ }
229
+ })
230
+ .catch(() => {
231
+ onError();
232
+ });
553
233
  };
554
234
  this.checkAndUpdateSkipSwapHistoryRecursive = (id, onFulfill, onError) => {
555
235
  const history = this.getRecentSkipHistory(id);
@@ -576,21 +256,10 @@ class RecentSendHistoryService {
576
256
  onFulfill();
577
257
  return;
578
258
  }
579
- // Skip API에 보낼 request 정보
580
- const request = {
581
- tx_hash: txHash,
582
- chain_id: chainId.replace("eip155:", ""),
583
- };
584
- const requestParams = new URLSearchParams(request).toString();
585
- (0, simple_fetch_1.simpleFetch)(SWAP_API_ENDPOINT, `/v1/swap/tx?${requestParams}`, {
586
- method: "GET",
587
- headers: Object.assign({ "content-type": "application/json" }, (() => {
588
- const res = {};
589
- if (process.env["SKIP_API_KEY"]) {
590
- res.authorization = process.env["SKIP_API_KEY"];
591
- }
592
- return res;
593
- })()),
259
+ (0, api_1.requestSkipTxStatus)({
260
+ endpoint: SWAP_API_ENDPOINT,
261
+ chainId: chainId.replace("eip155:", ""),
262
+ txHash,
594
263
  })
595
264
  .then((res) => {
596
265
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
@@ -835,7 +504,7 @@ class RecentSendHistoryService {
835
504
  history.routeIndex = simpleRoute.length - 1;
836
505
  }
837
506
  if (receiveTxHash) {
838
- this.trackDestinationAssetAmount(id, receiveTxHash, onFulfill);
507
+ this.trackSkipDestinationAssetAmount(id, receiveTxHash, onFulfill);
839
508
  }
840
509
  else {
841
510
  history.trackDone = true;
@@ -854,36 +523,35 @@ class RecentSendHistoryService {
854
523
  onError();
855
524
  });
856
525
  };
526
+ // ============================================================================
527
+ // Chain removed handler
528
+ // ============================================================================
857
529
  this.onChainRemoved = (chainInfo) => {
858
530
  const chainIdentifier = cosmos_1.ChainIdHelper.parse(chainInfo.chainId).identifier;
859
- (0, mobx_1.runInAction)(() => {
860
- const removingIds = [];
861
- for (const history of this.recentIBCHistoryMap.values()) {
862
- if (cosmos_1.ChainIdHelper.parse(history.chainId).identifier === chainIdentifier) {
863
- removingIds.push(history.id);
864
- continue;
865
- }
866
- if (cosmos_1.ChainIdHelper.parse(history.destinationChainId).identifier ===
867
- chainIdentifier) {
868
- removingIds.push(history.id);
869
- continue;
870
- }
871
- if (history.ibcHistory.some((history) => {
872
- return (cosmos_1.ChainIdHelper.parse(history.counterpartyChainId).identifier ===
873
- chainIdentifier);
874
- })) {
875
- removingIds.push(history.id);
876
- continue;
877
- }
878
- }
879
- for (const id of removingIds) {
880
- this.recentIBCHistoryMap.delete(id);
881
- }
882
- });
531
+ try {
532
+ this.removeIBCHistoriesByChainIdentifier(chainIdentifier);
533
+ this.removeSkipHistoriesByChainIdentifier(chainIdentifier);
534
+ this.removeSwapV2HistoriesByChainIdentifier(chainIdentifier);
535
+ }
536
+ catch (e) {
537
+ console.error(e);
538
+ }
883
539
  };
884
540
  (0, mobx_1.makeObservable)(this);
885
541
  }
542
+ // ============================================================================
543
+ // Init – load & persist histories
544
+ // ============================================================================
886
545
  init() {
546
+ return __awaiter(this, void 0, void 0, function* () {
547
+ yield this.initRecentSendHistory();
548
+ yield this.initRecentIBCHistory();
549
+ yield this.initRecentSkipHistory();
550
+ yield this.initRecentSwapV2History();
551
+ this.chainsService.addChainRemovedHandler(this.onChainRemoved);
552
+ });
553
+ }
554
+ initRecentSendHistory() {
887
555
  return __awaiter(this, void 0, void 0, function* () {
888
556
  const recentSendHistoryMapSaved = yield this.kvStore.get("recentSendHistoryMap");
889
557
  if (recentSendHistoryMapSaved) {
@@ -898,6 +566,10 @@ class RecentSendHistoryService {
898
566
  const obj = Object.fromEntries(js);
899
567
  this.kvStore.set("recentSendHistoryMap", obj);
900
568
  });
569
+ });
570
+ }
571
+ initRecentIBCHistory() {
572
+ return __awaiter(this, void 0, void 0, function* () {
901
573
  // 밑의 storage의 key들이 ibc transfer를 포함하는데
902
574
  // 이 이유는 이전에 transfer history만 지원되었을때
903
575
  // key를 그렇게 정했었기 때문이다
@@ -934,8 +606,14 @@ class RecentSendHistoryService {
934
606
  this.kvStore.set("recentIBCTransferHistoryMap", obj);
935
607
  });
936
608
  for (const history of this.getRecentIBCHistories()) {
937
- this.trackIBCPacketForwardingRecursive(history.id);
609
+ this.trackIBCPacketForwardingRecursive((onFulfill, onClose, onError) => {
610
+ this.trackIBCPacketForwardingRecursiveInternal(history.id, onFulfill, onClose, onError);
611
+ });
938
612
  }
613
+ });
614
+ }
615
+ initRecentSkipHistory() {
616
+ return __awaiter(this, void 0, void 0, function* () {
939
617
  // Load skip history sequence from the storage
940
618
  const recentSkipHistorySeqSaved = yield this.kvStore.get("recentSkipHistorySeq");
941
619
  if (recentSkipHistorySeqSaved) {
@@ -972,9 +650,51 @@ class RecentSendHistoryService {
972
650
  for (const history of this.getRecentSkipHistories()) {
973
651
  this.trackSkipSwapRecursive(history.id);
974
652
  }
975
- this.chainsService.addChainRemovedHandler(this.onChainRemoved);
976
653
  });
977
654
  }
655
+ initRecentSwapV2History() {
656
+ return __awaiter(this, void 0, void 0, function* () {
657
+ const recentSwapV2HistorySeqSaved = yield this.kvStore.get("recentSwapV2HistorySeq");
658
+ if (recentSwapV2HistorySeqSaved) {
659
+ (0, mobx_1.runInAction)(() => {
660
+ this.recentSwapV2HistorySeq = recentSwapV2HistorySeqSaved;
661
+ });
662
+ }
663
+ // Save the swap v2 history sequence to the storage when the swap v2 history sequence is changed
664
+ (0, mobx_1.autorun)(() => {
665
+ const js = (0, mobx_1.toJS)(this.recentSwapV2HistorySeq);
666
+ this.kvStore.set("recentSwapV2HistorySeq", js);
667
+ });
668
+ // Load swap v2 history from the storage
669
+ const recentSwapV2HistoryMapSaved = yield this.kvStore.get("recentSwapV2HistoryMap");
670
+ if (recentSwapV2HistoryMapSaved) {
671
+ (0, mobx_1.runInAction)(() => {
672
+ let entries = Object.entries(recentSwapV2HistoryMapSaved);
673
+ entries = entries.sort(([, a], [, b]) => {
674
+ return parseInt(a.id) - parseInt(b.id);
675
+ });
676
+ for (const [key, value] of entries) {
677
+ this.recentSwapV2HistoryMap.set(key, value);
678
+ }
679
+ });
680
+ }
681
+ // Save the swap v2 history to the storage when the swap v2 history is changed
682
+ (0, mobx_1.autorun)(() => {
683
+ const js = (0, mobx_1.toJS)(this.recentSwapV2HistoryMap);
684
+ const obj = Object.fromEntries(js);
685
+ this.kvStore.set("recentSwapV2HistoryMap", obj);
686
+ });
687
+ for (const history of this.getRecentSwapV2Histories()) {
688
+ this.trackSwapV2Recursive(history.id);
689
+ if (history.additionalTrackingData && !history.additionalTrackDone) {
690
+ this.trackSwapV2AdditionalRecursive(history.id);
691
+ }
692
+ }
693
+ });
694
+ }
695
+ // ============================================================================
696
+ // Send tx and record
697
+ // ============================================================================
978
698
  sendTxAndRecord(type, sourceChainId, destinationChainId, tx, mode, silent, sender, recipient, amount, memo, ibcChannels, notificationInfo, shouldLegacyTrack = false) {
979
699
  var _a, _b;
980
700
  return __awaiter(this, void 0, void 0, function* () {
@@ -1004,19 +724,10 @@ class RecentSendHistoryService {
1004
724
  if (shouldLegacyTrack) {
1005
725
  // no wait
1006
726
  setTimeout(() => {
1007
- (0, simple_fetch_1.simpleFetch)(SWAP_API_ENDPOINT, "/v1/swap/tx", {
1008
- method: "POST",
1009
- headers: Object.assign({ "content-type": "application/json" }, (() => {
1010
- const res = {};
1011
- if (process.env["SKIP_API_KEY"]) {
1012
- res.authorization = process.env["SKIP_API_KEY"];
1013
- }
1014
- return res;
1015
- })()),
1016
- body: JSON.stringify({
1017
- tx_hash: buffer_1.Buffer.from(tx.hash).toString("hex"),
1018
- chain_id: sourceChainId,
1019
- }),
727
+ (0, api_1.requestSkipTxTrack)({
728
+ endpoint: SWAP_API_ENDPOINT,
729
+ chainId: sourceChainId,
730
+ txHash: buffer_1.Buffer.from(tx.hash).toString("hex"),
1020
731
  })
1021
732
  .then((result) => {
1022
733
  console.log(`Skip tx track result: ${JSON.stringify(result)}`);
@@ -1032,11 +743,29 @@ class RecentSendHistoryService {
1032
743
  });
1033
744
  if (ibcChannels && ibcChannels.length > 0) {
1034
745
  const id = this.addRecentIBCTransferHistory(sourceChainId, destinationChainId, sender, recipient, amount, memo, ibcChannels, notificationInfo, txHash);
1035
- this.trackIBCPacketForwardingRecursive(id);
746
+ this.trackIBCPacketForwardingRecursive((onFulfill, onClose, onError) => {
747
+ this.trackIBCPacketForwardingRecursiveInternal(id, onFulfill, onClose, onError);
748
+ });
1036
749
  }
1037
750
  return txHash;
1038
751
  });
1039
752
  }
753
+ getRecentSendHistories(chainId, type) {
754
+ var _a;
755
+ const key = `${cosmos_1.ChainIdHelper.parse(chainId).identifier}/${type}`;
756
+ return ((_a = this.recentSendHistoryMap.get(key)) !== null && _a !== void 0 ? _a : []).slice(0, 20);
757
+ }
758
+ addRecentSendHistory(chainId, type, history) {
759
+ var _a;
760
+ const key = `${cosmos_1.ChainIdHelper.parse(chainId).identifier}/${type}`;
761
+ let histories = (_a = this.recentSendHistoryMap.get(key)) !== null && _a !== void 0 ? _a : [];
762
+ histories.unshift(Object.assign({ timestamp: Date.now() }, history));
763
+ histories = histories.slice(0, 20);
764
+ this.recentSendHistoryMap.set(key, histories);
765
+ }
766
+ // ============================================================================
767
+ // Send tx and record IBC swap/transfer
768
+ // ============================================================================
1040
769
  sendTxAndRecordIBCSwap(swapType, sourceChainId, destinationChainId, tx, mode, silent, sender, amount, memo, ibcChannels, destinationAsset, swapChannelIndex, swapReceiver, notificationInfo, shouldLegacyTrack = false) {
1041
770
  var _a;
1042
771
  return __awaiter(this, void 0, void 0, function* () {
@@ -1051,19 +780,10 @@ class RecentSendHistoryService {
1051
780
  if (shouldLegacyTrack) {
1052
781
  setTimeout(() => {
1053
782
  // no wait
1054
- (0, simple_fetch_1.simpleFetch)(SWAP_API_ENDPOINT, "/v1/swap/tx", {
1055
- method: "POST",
1056
- headers: Object.assign({ "content-type": "application/json" }, (() => {
1057
- const res = {};
1058
- if (process.env["SKIP_API_KEY"]) {
1059
- res.authorization = process.env["SKIP_API_KEY"];
1060
- }
1061
- return res;
1062
- })()),
1063
- body: JSON.stringify({
1064
- tx_hash: buffer_1.Buffer.from(tx.hash).toString("hex"),
1065
- chain_id: sourceChainId,
1066
- }),
783
+ (0, api_1.requestSkipTxTrack)({
784
+ endpoint: SWAP_API_ENDPOINT,
785
+ chainId: sourceChainId,
786
+ txHash: buffer_1.Buffer.from(tx.hash).toString("hex"),
1067
787
  })
1068
788
  .then((result) => {
1069
789
  console.log(`Skip tx track result: ${JSON.stringify(result)}`);
@@ -1079,49 +799,13 @@ class RecentSendHistoryService {
1079
799
  });
1080
800
  if (shouldLegacyTrack) {
1081
801
  const id = this.addRecentIBCSwapHistory(swapType, sourceChainId, destinationChainId, sender, amount, memo, ibcChannels, destinationAsset, swapChannelIndex, swapReceiver, notificationInfo, txHash);
1082
- this.trackIBCPacketForwardingRecursive(id);
802
+ this.trackIBCPacketForwardingRecursive((onFulfill, onClose, onError) => {
803
+ this.trackIBCPacketForwardingRecursiveInternal(id, onFulfill, onClose, onError);
804
+ });
1083
805
  }
1084
806
  return txHash;
1085
807
  });
1086
808
  }
1087
- trackIBCPacketForwardingRecursive(id) {
1088
- (0, common_1.retry)(() => {
1089
- return new Promise((resolve, reject) => {
1090
- this.trackIBCPacketForwardingRecursiveInternal(id, () => {
1091
- resolve();
1092
- }, () => {
1093
- // reject if ws closed before fulfilled
1094
- // 하지만 로직상 fulfill 되기 전에 ws가 닫히는게 되기 때문에
1095
- // delay를 좀 준다.
1096
- // 현재 trackIBCPacketForwardingRecursiveInternal에 ws close 이후에는 동기적인 로직밖에 없으므로
1097
- // 문제될게 없다.
1098
- setTimeout(() => {
1099
- reject();
1100
- }, 500);
1101
- }, () => {
1102
- // reject if ws error occurred before fulfilled
1103
- reject();
1104
- });
1105
- });
1106
- }, {
1107
- maxRetries: 10,
1108
- waitMsAfterError: 10 * 1000,
1109
- maxWaitMsAfterError: 5 * 60 * 1000, // 5min
1110
- });
1111
- }
1112
- getRecentSendHistories(chainId, type) {
1113
- var _a;
1114
- const key = `${cosmos_1.ChainIdHelper.parse(chainId).identifier}/${type}`;
1115
- return ((_a = this.recentSendHistoryMap.get(key)) !== null && _a !== void 0 ? _a : []).slice(0, 20);
1116
- }
1117
- addRecentSendHistory(chainId, type, history) {
1118
- var _a;
1119
- const key = `${cosmos_1.ChainIdHelper.parse(chainId).identifier}/${type}`;
1120
- let histories = (_a = this.recentSendHistoryMap.get(key)) !== null && _a !== void 0 ? _a : [];
1121
- histories.unshift(Object.assign({ timestamp: Date.now() }, history));
1122
- histories = histories.slice(0, 20);
1123
- this.recentSendHistoryMap.set(key, histories);
1124
- }
1125
809
  addRecentIBCTransferHistory(chainId, destinationChainId, sender, recipient, amount, memo, ibcChannels, notificationInfo, txHash) {
1126
810
  const id = (this.recentIBCHistorySeq++).toString();
1127
811
  const history = {
@@ -1201,16 +885,311 @@ class RecentSendHistoryService {
1201
885
  clearAllRecentIBCHistory() {
1202
886
  this.recentIBCHistoryMap.clear();
1203
887
  }
1204
- // skip related methods
1205
- recordTxWithSkipSwap(sourceChainId, destinationChainId, destinationAsset, simpleRoute, sender, recipient, amount, notificationInfo, routeDurationSeconds = 0, txHash, isOnlyUseBridge) {
1206
- const id = (this.recentIBCHistorySeq++).toString();
1207
- const history = {
1208
- id,
1209
- chainId: sourceChainId,
1210
- destinationChainId: destinationChainId,
1211
- destinationAsset,
1212
- simpleRoute,
1213
- sender,
888
+ // ============================================================================
889
+ // Common functions for history tracking (IBC, Skip, Swap V2)
890
+ // ============================================================================
891
+ trackIBCPacketForwardingRecursive(trackHandler) {
892
+ (0, common_1.retry)(() => {
893
+ return new Promise((resolve, reject) => {
894
+ trackHandler(() => {
895
+ resolve();
896
+ }, () => {
897
+ // reject if ws closed before fulfilled
898
+ // 하지만 로직상 fulfill 되기 전에 ws가 닫히는게 되기 때문에
899
+ // delay를 좀 준다.
900
+ // 현재 trackIBCPacketForwardingRecursiveInternal에 ws close 이후에는 동기적인 로직밖에 없으므로
901
+ // 문제될게 없다.
902
+ setTimeout(() => {
903
+ reject();
904
+ }, 500);
905
+ }, () => {
906
+ // reject if ws error occurred before fulfilled
907
+ reject();
908
+ });
909
+ });
910
+ }, {
911
+ maxRetries: 10,
912
+ waitMsAfterError: 10 * 1000,
913
+ maxWaitMsAfterError: 5 * 60 * 1000, // 5min
914
+ });
915
+ }
916
+ resolveTxExecutionStatus(chainInfo, chainId, txHash) {
917
+ return __awaiter(this, void 0, void 0, function* () {
918
+ if (this.chainsService.isEvmChain(chainId)) {
919
+ const evmInfo = chainInfo.evm;
920
+ if (!evmInfo) {
921
+ return Promise.resolve("error");
922
+ }
923
+ const res = yield (0, api_1.requestEthTxReceipt)({
924
+ rpc: evmInfo.rpc,
925
+ txHash,
926
+ origin,
927
+ });
928
+ if (res.data.error) {
929
+ return "error";
930
+ }
931
+ const txReceipt = res.data.result;
932
+ if (!txReceipt) {
933
+ return "pending";
934
+ }
935
+ if (txReceipt.status === types_2.EthTxStatus.Success) {
936
+ return "success";
937
+ }
938
+ return "failed";
939
+ }
940
+ const txTracer = new cosmos_1.TendermintTxTracer(chainInfo.rpc, "/websocket");
941
+ txTracer.addEventListener("error", () => {
942
+ txTracer.close();
943
+ });
944
+ return txTracer
945
+ .traceTx(buffer_1.Buffer.from(txHash.replace("0x", ""), "hex"))
946
+ .then((res) => {
947
+ txTracer.close();
948
+ const txResult = Array.isArray(res.txs)
949
+ ? res.txs && res.txs.length > 0
950
+ ? res.txs[0].tx_result
951
+ : undefined
952
+ : res;
953
+ if (!txResult) {
954
+ return "pending";
955
+ }
956
+ if (typeof txResult.code !== "number") {
957
+ return "error";
958
+ }
959
+ return txResult.code === 0 ? "success" : "failed";
960
+ })
961
+ .catch(() => {
962
+ txTracer.close();
963
+ return "error";
964
+ });
965
+ });
966
+ }
967
+ trackDestinationAssetAmount(params) {
968
+ const { chainId, txHash, recipient, targetDenom, onResult, onRefund, onFulfill, } = params;
969
+ const chainInfo = this.chainsService.getChainInfo(chainId);
970
+ if (!chainInfo) {
971
+ onFulfill();
972
+ return;
973
+ }
974
+ if (this.chainsService.isEvmChain(chainId)) {
975
+ this.traceEVMTransactionResult({
976
+ chainId,
977
+ txHash,
978
+ recipient,
979
+ targetDenom,
980
+ onResult: (result) => {
981
+ if (result.resAmount) {
982
+ onResult(result.resAmount);
983
+ }
984
+ if (result.refundInfo && onRefund) {
985
+ onRefund(result.refundInfo, result.error);
986
+ }
987
+ },
988
+ onFulfill,
989
+ });
990
+ return;
991
+ }
992
+ this.traceCosmosTransactionResult({
993
+ chainInfo,
994
+ txHash,
995
+ recipient,
996
+ onResult,
997
+ onFulfill,
998
+ });
999
+ }
1000
+ traceCosmosTransactionResult(params) {
1001
+ const { chainInfo, txHash, recipient, onResult, onFulfill } = params;
1002
+ const txTracer = new cosmos_1.TendermintTxTracer(chainInfo.rpc, "/websocket");
1003
+ txTracer.addEventListener("error", () => onFulfill());
1004
+ txTracer
1005
+ .queryTx({
1006
+ "tx.hash": txHash,
1007
+ })
1008
+ .then((res) => {
1009
+ txTracer.close();
1010
+ if (!res) {
1011
+ return;
1012
+ }
1013
+ const txs = res.txs
1014
+ ? res.txs.map((r) => r.tx_result || r)
1015
+ : [res.tx_result || res];
1016
+ for (const tx of txs) {
1017
+ const resAmount = this.getIBCSwapResAmountFromTx(tx, recipient);
1018
+ onResult(resAmount);
1019
+ return;
1020
+ }
1021
+ })
1022
+ .finally(() => {
1023
+ onFulfill();
1024
+ });
1025
+ }
1026
+ // CHECK: move tracing logic (requestEthTxReceipt, requestEthTxTrace, parseEVMTxReceiptLogs) to tx-ethereum service
1027
+ traceEVMTransactionResult(params) {
1028
+ const { chainId, txHash, recipient, targetDenom, onResult, onFulfill } = params;
1029
+ const chainInfo = this.chainsService.getChainInfo(chainId);
1030
+ if (!chainInfo) {
1031
+ onResult({ success: false });
1032
+ onFulfill();
1033
+ return;
1034
+ }
1035
+ if (!this.chainsService.isEvmChain(chainId)) {
1036
+ onResult({ success: false, error: "Not an EVM chain" });
1037
+ onFulfill();
1038
+ return;
1039
+ }
1040
+ const evmInfo = chainInfo.evm;
1041
+ if (!evmInfo) {
1042
+ onResult({ success: false });
1043
+ onFulfill();
1044
+ return;
1045
+ }
1046
+ (0, api_1.requestEthTxReceipt)({
1047
+ rpc: evmInfo.rpc,
1048
+ txHash,
1049
+ origin,
1050
+ })
1051
+ .then((res) => {
1052
+ const txReceipt = res.data.result;
1053
+ if (!txReceipt) {
1054
+ onResult({ success: false });
1055
+ return;
1056
+ }
1057
+ (0, api_1.requestEthTxTrace)({
1058
+ rpc: evmInfo.rpc,
1059
+ txHash,
1060
+ origin,
1061
+ }).then((traceRes) => {
1062
+ let isFoundFromCall = false;
1063
+ const foundResAmount = [];
1064
+ if (traceRes.data.result) {
1065
+ const searchForTransfers = (calls) => {
1066
+ var _a, _b;
1067
+ for (const call of calls) {
1068
+ if (call.type === "CALL" &&
1069
+ ((_a = call.to) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === recipient.toLowerCase()) {
1070
+ const isERC20Transfer = (_b = call.input) === null || _b === void 0 ? void 0 : _b.startsWith("0xa9059cbb");
1071
+ const value = BigInt(isERC20Transfer
1072
+ ? `0x${call.input.substring(74)}`
1073
+ : call.value || "0x0");
1074
+ foundResAmount.push({
1075
+ amount: value.toString(10),
1076
+ denom: targetDenom,
1077
+ });
1078
+ isFoundFromCall = true;
1079
+ }
1080
+ if (call.calls && call.calls.length > 0) {
1081
+ searchForTransfers(call.calls);
1082
+ }
1083
+ }
1084
+ };
1085
+ searchForTransfers(traceRes.data.result.calls || []);
1086
+ }
1087
+ if (isFoundFromCall) {
1088
+ onResult({ success: true, resAmount: foundResAmount });
1089
+ return;
1090
+ }
1091
+ // fallback to logs if debug_traceTransaction fails
1092
+ this.parseEVMTxReceiptLogs({
1093
+ txReceipt,
1094
+ recipient,
1095
+ targetChainId: chainId,
1096
+ targetDenom,
1097
+ onResult,
1098
+ });
1099
+ });
1100
+ })
1101
+ .finally(() => {
1102
+ onFulfill();
1103
+ });
1104
+ }
1105
+ parseEVMTxReceiptLogs(params) {
1106
+ var _a;
1107
+ const { txReceipt, recipient, targetChainId, targetDenom, onResult } = params;
1108
+ const logs = txReceipt.logs;
1109
+ const transferTopic = (0, hash_1.id)("Transfer(address,address,uint256)");
1110
+ const withdrawTopic = (0, hash_1.id)("Withdrawal(address,uint256)");
1111
+ const hyperlaneReceiveTopic = (0, hash_1.id)("ReceivedTransferRemote(uint32,bytes32,uint256)");
1112
+ for (const log of logs) {
1113
+ if (log.topics[0] === transferTopic) {
1114
+ const to = "0x" + log.topics[2].slice(26);
1115
+ if (to.toLowerCase() === recipient.toLowerCase()) {
1116
+ const expectedAssetDenom = targetDenom.replace("erc20:", "");
1117
+ const amount = BigInt(log.data).toString(10);
1118
+ if (log.address.toLowerCase() === expectedAssetDenom.toLowerCase()) {
1119
+ onResult({
1120
+ success: true,
1121
+ resAmount: [{ amount, denom: targetDenom }],
1122
+ });
1123
+ }
1124
+ else {
1125
+ console.log("refunded", log.address);
1126
+ // Transfer 토픽인 경우엔 ERC20의 tranfer 호출일텐데
1127
+ // 받을 토큰의 컨트랙트가 아닌 다른 컨트랙트에서 호출된 경우는 Swap을 실패한 것으로 추측
1128
+ // 고로 실제로 받은 토큰의 컨트랙트 주소로 환불 정보에 저장한다.
1129
+ onResult({
1130
+ success: false,
1131
+ error: "Swap failed",
1132
+ refundInfo: {
1133
+ chainId: targetChainId,
1134
+ amount: [
1135
+ {
1136
+ amount,
1137
+ denom: `erc20:${log.address.toLowerCase()}`,
1138
+ },
1139
+ ],
1140
+ },
1141
+ });
1142
+ }
1143
+ return;
1144
+ }
1145
+ }
1146
+ else if (log.topics[0] === withdrawTopic) {
1147
+ const to = "0x" + log.topics[1].slice(26);
1148
+ if (to.toLowerCase() === ((_a = txReceipt.to) === null || _a === void 0 ? void 0 : _a.toLowerCase())) {
1149
+ const amount = BigInt(log.data).toString(10);
1150
+ onResult({
1151
+ success: true,
1152
+ resAmount: [{ amount, denom: targetDenom }],
1153
+ });
1154
+ return;
1155
+ }
1156
+ }
1157
+ else if (log.topics[0] === hyperlaneReceiveTopic) {
1158
+ const to = "0x" + log.topics[2].slice(26);
1159
+ if (to.toLowerCase() === recipient.toLowerCase()) {
1160
+ const amount = BigInt(log.data).toString(10);
1161
+ // Hyperlane을 통해 Forma로 TIA를 받는 경우 토큰 수량이 decimal 6으로 기록되는데,
1162
+ // Forma에서는 decimal 18이기 때문에 12자리 만큼 0을 붙여준다.
1163
+ onResult({
1164
+ success: true,
1165
+ resAmount: [
1166
+ {
1167
+ amount: targetDenom === "forma-native"
1168
+ ? `${amount}000000000000`
1169
+ : amount,
1170
+ denom: targetDenom,
1171
+ },
1172
+ ],
1173
+ });
1174
+ return;
1175
+ }
1176
+ }
1177
+ }
1178
+ // 결과를 찾지 못한 경우
1179
+ onResult({ success: false });
1180
+ }
1181
+ // ============================================================================
1182
+ // Skip swap history
1183
+ // ============================================================================
1184
+ recordTxWithSkipSwap(sourceChainId, destinationChainId, destinationAsset, simpleRoute, sender, recipient, amount, notificationInfo, routeDurationSeconds = 0, txHash, isOnlyUseBridge) {
1185
+ const id = (this.recentIBCHistorySeq++).toString();
1186
+ const history = {
1187
+ id,
1188
+ chainId: sourceChainId,
1189
+ destinationChainId: destinationChainId,
1190
+ destinationAsset,
1191
+ simpleRoute,
1192
+ sender,
1214
1193
  recipient,
1215
1194
  amount,
1216
1195
  notificationInfo,
@@ -1228,241 +1207,1302 @@ class RecentSendHistoryService {
1228
1207
  getRecentSkipHistory(id) {
1229
1208
  return this.recentSkipHistoryMap.get(id);
1230
1209
  }
1231
- getRecentSkipHistories() {
1232
- return Array.from(this.recentSkipHistoryMap.values()).filter((history) => {
1233
- if (!this.chainsService.hasChainInfo(history.chainId)) {
1234
- return false;
1210
+ getRecentSkipHistories() {
1211
+ return Array.from(this.recentSkipHistoryMap.values()).filter((history) => {
1212
+ if (!this.chainsService.hasChainInfo(history.chainId)) {
1213
+ return false;
1214
+ }
1215
+ if (!this.chainsService.hasChainInfo(history.destinationChainId)) {
1216
+ return false;
1217
+ }
1218
+ if (history.simpleRoute.some((route) => {
1219
+ return !this.chainsService.hasChainInfo(route.chainId);
1220
+ })) {
1221
+ return false;
1222
+ }
1223
+ return true;
1224
+ });
1225
+ }
1226
+ trackSkipSwapRecursive(id) {
1227
+ const history = this.getRecentSkipHistory(id);
1228
+ if (!history) {
1229
+ return;
1230
+ }
1231
+ // check tx fulfilled and update history
1232
+ (0, common_1.retry)(() => {
1233
+ return new Promise((txFulfilledResolve, txFulfilledReject) => {
1234
+ this.checkAndTrackSwapTxFulfilledRecursive({
1235
+ chainId: history.chainId,
1236
+ txHash: history.txHash,
1237
+ onSuccess: () => {
1238
+ this.requestSkipTxTrackInternal({
1239
+ chainId: history.chainId,
1240
+ txHash: history.txHash,
1241
+ onRemoveHistory: () => this.removeRecentSkipHistory(id),
1242
+ onFulfill: (keepTracking) => {
1243
+ txFulfilledResolve();
1244
+ if (!keepTracking) {
1245
+ return;
1246
+ }
1247
+ (0, common_1.retry)(() => {
1248
+ return new Promise((resolve, reject) => {
1249
+ this.checkAndUpdateSkipSwapHistoryRecursive(id, resolve, reject);
1250
+ });
1251
+ }, {
1252
+ maxRetries: 50,
1253
+ waitMsAfterError: 500,
1254
+ maxWaitMsAfterError: 15000,
1255
+ });
1256
+ },
1257
+ });
1258
+ },
1259
+ onPending: txFulfilledReject,
1260
+ onFailed: () => {
1261
+ this.removeRecentSkipHistory(id);
1262
+ txFulfilledResolve();
1263
+ },
1264
+ onError: () => {
1265
+ txFulfilledResolve();
1266
+ },
1267
+ });
1268
+ });
1269
+ }, {
1270
+ maxRetries: 50,
1271
+ waitMsAfterError: 500,
1272
+ maxWaitMsAfterError: 15000,
1273
+ });
1274
+ }
1275
+ requestSkipTxTrackInternal(params) {
1276
+ const { chainId, txHash, onFulfill, onRemoveHistory } = params;
1277
+ const chainIdForApi = this.chainsService.isEvmChain(chainId)
1278
+ ? chainId.replace("eip155:", "")
1279
+ : chainId;
1280
+ setTimeout(() => {
1281
+ (0, api_1.requestSkipTxTrack)({
1282
+ endpoint: SWAP_API_ENDPOINT,
1283
+ chainId: chainIdForApi,
1284
+ txHash,
1285
+ })
1286
+ .then((result) => {
1287
+ console.log(`Skip tx track result: ${JSON.stringify(result)}`);
1288
+ onFulfill(true);
1289
+ })
1290
+ .catch((e) => {
1291
+ console.log(e);
1292
+ onRemoveHistory();
1293
+ onFulfill(false);
1294
+ });
1295
+ }, 2000);
1296
+ }
1297
+ trackSkipDestinationAssetAmount(historyId, txHash, onFulfill) {
1298
+ const history = this.getRecentSkipHistory(historyId);
1299
+ if (!history) {
1300
+ onFulfill();
1301
+ return;
1302
+ }
1303
+ const chainInfo = this.chainsService.getChainInfo(history.destinationChainId);
1304
+ if (!chainInfo) {
1305
+ onFulfill();
1306
+ return;
1307
+ }
1308
+ this.trackDestinationAssetAmount({
1309
+ chainId: history.destinationChainId,
1310
+ txHash,
1311
+ recipient: history.recipient,
1312
+ targetDenom: history.destinationAsset.denom,
1313
+ onResult: (resAmount) => {
1314
+ (0, mobx_1.runInAction)(() => {
1315
+ history.resAmount.push(resAmount);
1316
+ history.trackDone = true;
1317
+ });
1318
+ },
1319
+ onRefund: (refundInfo, error) => {
1320
+ (0, mobx_1.runInAction)(() => {
1321
+ history.trackError = error;
1322
+ history.swapRefundInfo = refundInfo;
1323
+ history.trackDone = true;
1324
+ });
1325
+ },
1326
+ onFulfill: () => {
1327
+ // ensure completion even if no result parsed
1328
+ (0, mobx_1.runInAction)(() => {
1329
+ history.trackDone = true;
1330
+ });
1331
+ onFulfill();
1332
+ },
1333
+ });
1334
+ }
1335
+ removeRecentSkipHistory(id) {
1336
+ return this.recentSkipHistoryMap.delete(id);
1337
+ }
1338
+ clearAllRecentSkipHistory() {
1339
+ this.recentSkipHistoryMap.clear();
1340
+ }
1341
+ // ============================================================================
1342
+ // Swap V2 history
1343
+ // ============================================================================
1344
+ recordTxWithSwapV2(fromChainId, toChainId, provider, destinationAsset, simpleRoute, sender, recipient, amount, notificationInfo, routeDurationSeconds = 0, txHash, isOnlyUseBridge, backgroundExecutionId) {
1345
+ const id = (this.recentSwapV2HistorySeq++).toString();
1346
+ const history = {
1347
+ id,
1348
+ fromChainId,
1349
+ toChainId,
1350
+ provider,
1351
+ timestamp: Date.now(),
1352
+ sender,
1353
+ recipient,
1354
+ amount,
1355
+ notificationInfo,
1356
+ routeDurationSeconds,
1357
+ txHash,
1358
+ isOnlyUseBridge,
1359
+ status: types_1.SwapV2TxStatus.IN_PROGRESS,
1360
+ simpleRoute,
1361
+ routeIndex: -1,
1362
+ destinationAsset,
1363
+ resAmount: [],
1364
+ assetLocationInfo: undefined,
1365
+ notified: undefined,
1366
+ backgroundExecutionId,
1367
+ };
1368
+ this.recentSwapV2HistoryMap.set(id, history);
1369
+ this.trackSwapV2Recursive(id);
1370
+ return id;
1371
+ }
1372
+ trackSwapV2Recursive(id) {
1373
+ const history = this.getRecentSwapV2History(id);
1374
+ if (!history) {
1375
+ return;
1376
+ }
1377
+ (0, common_1.retry)(() => {
1378
+ return new Promise((txFulfilledResolve, txFulfilledReject) => {
1379
+ this.checkAndTrackSwapTxFulfilledRecursive({
1380
+ chainId: history.fromChainId,
1381
+ txHash: history.txHash,
1382
+ onSuccess: () => {
1383
+ txFulfilledResolve();
1384
+ (0, common_1.retry)(() => {
1385
+ return new Promise((resolve, reject) => {
1386
+ this.checkAndUpdateSwapV2HistoryRecursive(id, resolve, reject);
1387
+ });
1388
+ }, {
1389
+ maxRetries: 60,
1390
+ waitMsAfterError: 1000,
1391
+ maxWaitMsAfterError: 45000,
1392
+ });
1393
+ },
1394
+ onPending: txFulfilledReject,
1395
+ onFailed: () => {
1396
+ this.removeRecentSwapV2History(id);
1397
+ txFulfilledResolve();
1398
+ },
1399
+ onError: txFulfilledResolve,
1400
+ });
1401
+ });
1402
+ }, {
1403
+ maxRetries: 60,
1404
+ waitMsAfterError: 1000,
1405
+ maxWaitMsAfterError: 45000,
1406
+ });
1407
+ }
1408
+ checkAndUpdateSwapV2HistoryRecursive(id, onFulfill, onError) {
1409
+ const history = this.getRecentSwapV2History(id);
1410
+ if (!history) {
1411
+ onFulfill();
1412
+ return;
1413
+ }
1414
+ // if already tracked, fulfill
1415
+ if (history.trackDone) {
1416
+ onFulfill();
1417
+ return;
1418
+ }
1419
+ const { txHash, fromChainId, toChainId, provider } = history;
1420
+ const normalizeChainId = (chainId) => {
1421
+ return chainId.replace("eip155:", "");
1422
+ };
1423
+ (0, api_1.requestSwapV2TxStatus)({
1424
+ endpoint: SWAP_API_ENDPOINT,
1425
+ fromChainId: normalizeChainId(fromChainId),
1426
+ toChainId: normalizeChainId(toChainId),
1427
+ provider,
1428
+ txHash,
1429
+ })
1430
+ .then((res) => {
1431
+ this.processSwapV2StatusResponse(id, res.data, onFulfill, onError);
1432
+ })
1433
+ .catch((e) => {
1434
+ console.error("SwapV2 status tracking error:", e);
1435
+ onError();
1436
+ });
1437
+ }
1438
+ processSwapV2StatusResponse(id, response, onFulfill, onError) {
1439
+ var _a, _b, _c;
1440
+ const history = this.getRecentSwapV2History(id);
1441
+ if (!history) {
1442
+ onFulfill();
1443
+ return;
1444
+ }
1445
+ const { status, steps, asset_location } = response;
1446
+ const { simpleRoute } = history;
1447
+ const prevRouteIndex = history.routeIndex;
1448
+ // 모든 상태 즉시 업데이트 (UNKNOWN 포함)
1449
+ history.status = status;
1450
+ history.trackError = undefined;
1451
+ // This might be the state where tracking has just started,
1452
+ // so handle the error and retry
1453
+ if (!steps || steps.length === 0) {
1454
+ if (status === types_1.SwapV2TxStatus.IN_PROGRESS ||
1455
+ status === types_1.SwapV2TxStatus.UNKNOWN) {
1456
+ onError();
1457
+ }
1458
+ else {
1459
+ // swap on single evm chain might not have steps
1460
+ history.trackDone = true;
1461
+ onFulfill();
1462
+ }
1463
+ return;
1464
+ }
1465
+ // find current step (not success first, otherwise last step)
1466
+ const currentStep = (_a = steps.find((s) => s.status !== types_1.SwapV2RouteStepStatus.SUCCESS)) !== null && _a !== void 0 ? _a : steps[steps.length - 1];
1467
+ const normalizeChainId = (chainId) => {
1468
+ return chainId.replace("eip155:", "").toLowerCase();
1469
+ };
1470
+ // Find the LAST step that matches the actual destination chain (history.toChainId)
1471
+ // Use reverse + find to handle routes that visit the destination chain multiple times
1472
+ // This handles cases where intermediate steps fail but the final destination is reached
1473
+ const destinationStep = [...steps].reverse().find((s) => {
1474
+ if (!s.chain_id) {
1475
+ return false;
1476
+ }
1477
+ return (normalizeChainId(s.chain_id) === normalizeChainId(history.toChainId));
1478
+ });
1479
+ const isDestinationStepSuccessful = destinationStep &&
1480
+ destinationStep.status === types_1.SwapV2RouteStepStatus.SUCCESS &&
1481
+ !!destinationStep.tx_hash;
1482
+ const findSimpleRouteIndex = (chainId) => {
1483
+ const normalizedChainId = normalizeChainId(chainId);
1484
+ for (let i = 0; i < simpleRoute.length; i++) {
1485
+ const routeChainId = normalizeChainId(simpleRoute[i].chainId);
1486
+ if (routeChainId === normalizedChainId) {
1487
+ return i;
1488
+ }
1489
+ }
1490
+ return -1;
1491
+ };
1492
+ // NOTE: The lengths of simpleRoute and steps may differ.
1493
+ let updatedRouteIndex = Math.max(0, history.routeIndex);
1494
+ // 1. Find highest completed simpleRoute index from all SUCCESS steps
1495
+ let highestCompletedIndex = -1;
1496
+ for (const step of steps) {
1497
+ if (step.status === types_1.SwapV2RouteStepStatus.SUCCESS && step.chain_id) {
1498
+ const routeIdx = findSimpleRouteIndex(step.chain_id);
1499
+ if (routeIdx > highestCompletedIndex) {
1500
+ highestCompletedIndex = routeIdx;
1501
+ }
1502
+ }
1503
+ }
1504
+ // 2. Also check if currentStep is in simpleRoute
1505
+ let currentStepIndex = -1;
1506
+ if (currentStep.chain_id) {
1507
+ currentStepIndex = findSimpleRouteIndex(currentStep.chain_id);
1508
+ }
1509
+ // 3. Use the higher value as updatedRouteIndex
1510
+ const candidateIndex = Math.max(highestCompletedIndex, currentStepIndex);
1511
+ if (candidateIndex >= 0) {
1512
+ updatedRouteIndex = candidateIndex;
1513
+ }
1514
+ const publishExecutableChains = (chainIds) => {
1515
+ if (!history.backgroundExecutionId) {
1516
+ return;
1517
+ }
1518
+ const executableChainIds = chainIds !== null && chainIds !== void 0 ? chainIds : this.getExecutableChainIdsFromSwapV2History(history);
1519
+ this.publisher.publish({
1520
+ type: "executable",
1521
+ executionId: history.backgroundExecutionId,
1522
+ executableChainIds,
1523
+ });
1524
+ };
1525
+ const isUnknownStatus = status === types_1.SwapV2TxStatus.UNKNOWN ||
1526
+ steps.some((s) => s.status === types_1.SwapV2RouteStepStatus.UNKNOWN);
1527
+ if (isUnknownStatus) {
1528
+ if (!history.unknownStatusFirstSeenAt) {
1529
+ // UNKNOWN 상태 처음 발견 - 타임스탬프 기록
1530
+ history.unknownStatusFirstSeenAt = Date.now();
1531
+ }
1532
+ else {
1533
+ const elapsedMs = Date.now() - history.unknownStatusFirstSeenAt;
1534
+ if (elapsedMs >= exports.UNKNOWN_TX_STATUS_TIMEOUT_MS) {
1535
+ this.analyticsService.logEventIgnoreError("swapV2UnknownTxStatusWithTimeout", {
1536
+ provider: history.provider,
1537
+ fromChainId: history.fromChainId,
1538
+ toChainId: history.toChainId,
1539
+ txHash: history.txHash,
1540
+ executedAt: history.timestamp,
1541
+ });
1542
+ history.trackDone = true;
1543
+ onFulfill();
1544
+ return;
1545
+ }
1546
+ }
1547
+ }
1548
+ else {
1549
+ // UNKNOWN 상태가 아니면 타임스탬프 초기화
1550
+ if (history.unknownStatusFirstSeenAt !== undefined) {
1551
+ history.unknownStatusFirstSeenAt = undefined;
1552
+ }
1553
+ }
1554
+ switch (status) {
1555
+ case types_1.SwapV2TxStatus.IN_PROGRESS:
1556
+ case types_1.SwapV2TxStatus.UNKNOWN:
1557
+ // publish executable chains if routeIndex increased
1558
+ if (updatedRouteIndex > prevRouteIndex) {
1559
+ history.routeIndex = updatedRouteIndex;
1560
+ publishExecutableChains();
1561
+ }
1562
+ // Continue polling
1563
+ onError();
1564
+ break;
1565
+ case types_1.SwapV2TxStatus.SUCCESS:
1566
+ case types_1.SwapV2TxStatus.PARTIAL_SUCCESS:
1567
+ case types_1.SwapV2TxStatus.FAILED:
1568
+ // If current step is still in progress, retry a few more times before finalizing
1569
+ if (currentStep.status === types_1.SwapV2RouteStepStatus.IN_PROGRESS) {
1570
+ const maxRetries = 3;
1571
+ const retryCount = (_b = history.finalizationRetryCount) !== null && _b !== void 0 ? _b : 0;
1572
+ if (retryCount < maxRetries) {
1573
+ history.finalizationRetryCount = retryCount + 1;
1574
+ onError();
1575
+ break;
1576
+ }
1577
+ // Max retries reached, fall through to finalize
1578
+ }
1579
+ let executableChainIdsToPublish;
1580
+ // NOTE: 현재 asset_location은 skip의 multichain operation인 경우에만 주어지는 값이다.
1581
+ if (asset_location) {
1582
+ const chainId = asset_location.chain_id;
1583
+ const evmLikeChainId = Number(chainId);
1584
+ const isEVMChainId = !Number.isNaN(evmLikeChainId) && evmLikeChainId > 0;
1585
+ const chainIdInKeplr = isEVMChainId ? `eip155:${chainId}` : chainId;
1586
+ const denomInKeplr = isEVMChainId
1587
+ ? `erc20:${asset_location.denom}`
1588
+ : asset_location.denom;
1589
+ // destination chain에 destination denom으로 도착했으면 완전 성공이므로
1590
+ // assetLocationInfo를 설정하지 않음
1591
+ const isDestinationReached = chainIdInKeplr === history.toChainId &&
1592
+ denomInKeplr.toLowerCase() ===
1593
+ history.destinationAsset.denom.toLowerCase();
1594
+ if (isDestinationReached) {
1595
+ history.routeIndex = simpleRoute.length - 1;
1596
+ executableChainIdsToPublish =
1597
+ this.getExecutableChainIdsFromSwapV2History(history, true);
1598
+ history.resAmount.push([
1599
+ {
1600
+ amount: asset_location.amount,
1601
+ denom: denomInKeplr,
1602
+ },
1603
+ ]);
1604
+ this.notifySwapV2Success(history);
1605
+ }
1606
+ else {
1607
+ /*
1608
+ Determine the type of asset location:
1609
+ - "intermediate": SUCCESS 상태이지만 asset_location이 최종 목적지가 아닌 경우
1610
+ (예: base USDC -> osmosis OSMO 스왑 시, noble USDC가 먼저 도착하고
1611
+ 이후 noble USDC -> osmosis OSMO로 ibc swap하는 transaction이 필요한 경우)
1612
+ 이 경우 추가 transaction을 실행하거나 현재 받은 자산을 그대로 둘 수 있음
1613
+ - "refund": 중간에서 또는 destination에서 스왑 실패 등으로 destination asset이 아닌 자산이 릴리즈된 경우
1614
+ backgroundExecutionId가 있으면 멀티 transaction 케이스이므로 다음 transaction을 실행할 수 있도록 'intermediate'로 설정
1615
+ */
1616
+ const assetLocationType = status === types_1.SwapV2TxStatus.SUCCESS && history.backgroundExecutionId
1617
+ ? "intermediate"
1618
+ : "refund";
1619
+ history.assetLocationInfo = {
1620
+ chainId: chainIdInKeplr,
1621
+ amount: [
1622
+ {
1623
+ amount: asset_location.amount,
1624
+ denom: denomInKeplr,
1625
+ },
1626
+ ],
1627
+ type: assetLocationType,
1628
+ };
1629
+ // refund 타입인 경우 status도 FAILED로 변경하여 UI에서 refund 상황을 표시할 수 있도록 함
1630
+ if (assetLocationType === "refund") {
1631
+ history.status = types_1.SwapV2TxStatus.FAILED;
1632
+ }
1633
+ // asset location chain까지 routeIndex가 이동해야 하는지 확인
1634
+ const assetLocationChainIndex = simpleRoute.findIndex((route) => route.chainId === chainIdInKeplr);
1635
+ if (assetLocationChainIndex !== -1 &&
1636
+ assetLocationChainIndex > updatedRouteIndex) {
1637
+ history.routeIndex = assetLocationChainIndex;
1638
+ }
1639
+ executableChainIdsToPublish =
1640
+ this.getExecutableChainIdsFromSwapV2History(history);
1641
+ }
1642
+ }
1643
+ else if (status === types_1.SwapV2TxStatus.SUCCESS) {
1644
+ // For SUCCESS without asset_location, move routeIndex to end
1645
+ history.routeIndex = simpleRoute.length - 1;
1646
+ executableChainIdsToPublish =
1647
+ this.getExecutableChainIdsFromSwapV2History(history, true);
1648
+ this.notifySwapV2Success(history);
1649
+ }
1650
+ // Publish executable chains
1651
+ publishExecutableChains(executableChainIdsToPublish);
1652
+ // destination 체인/denom 기준으로만 추가 추적을 시도한다.
1653
+ const targetChainId = history.toChainId;
1654
+ const targetDenom = history.destinationAsset.denom;
1655
+ // 해당 위치의 tx_hash를 찾아서 자산 추적, 없을 수도 있다.
1656
+ const targetTxHash = (_c = destinationStep === null || destinationStep === void 0 ? void 0 : destinationStep.tx_hash) !== null && _c !== void 0 ? _c : currentStep.tx_hash;
1657
+ const isAtDestinationChain = (() => {
1658
+ // Top-level SUCCESS + destination step successful → check against destination step
1659
+ if (status === types_1.SwapV2TxStatus.SUCCESS &&
1660
+ isDestinationStepSuccessful &&
1661
+ (destinationStep === null || destinationStep === void 0 ? void 0 : destinationStep.chain_id)) {
1662
+ return (normalizeChainId(destinationStep.chain_id) ===
1663
+ normalizeChainId(targetChainId));
1664
+ }
1665
+ // Default logic (other cases)
1666
+ if (!currentStep.chain_id) {
1667
+ return false;
1668
+ }
1669
+ return (normalizeChainId(currentStep.chain_id) ===
1670
+ normalizeChainId(targetChainId));
1671
+ })();
1672
+ const skipAssetTracking = history.resAmount.length > 0 || history.assetLocationInfo != null;
1673
+ // resAmount 또는 assetLocationInfo가 없으면 추가적으로 자산 추적을 해야 한다.
1674
+ if (targetTxHash && !skipAssetTracking && isAtDestinationChain) {
1675
+ console.log("trackSwapV2ReleasedAssetAmount", id, targetTxHash);
1676
+ this.trackSwapV2ReleasedAssetAmount(id, targetTxHash, targetChainId, targetDenom, onFulfill);
1677
+ }
1678
+ else if (status === types_1.SwapV2TxStatus.SUCCESS &&
1679
+ !skipAssetTracking &&
1680
+ !isAtDestinationChain) {
1681
+ // response status는 SUCCESS인데 destination chain에 도달하지 않음 → 실패 처리
1682
+ (0, mobx_1.runInAction)(() => {
1683
+ history.status = types_1.SwapV2TxStatus.FAILED;
1684
+ history.trackDone = true;
1685
+ });
1686
+ // TODO: additional tracking for failed case...
1687
+ onFulfill();
1688
+ }
1689
+ else {
1690
+ history.trackDone = true;
1691
+ onFulfill();
1692
+ }
1693
+ break;
1694
+ }
1695
+ }
1696
+ /**
1697
+ * Track released asset amount from tx receipt.
1698
+ * - SUCCESS: destination asset 추적
1699
+ * - FAILED/PARTIAL_SUCCESS + assetLocationInfo: refund된 자산 추적
1700
+ */
1701
+ trackSwapV2ReleasedAssetAmount(historyId, txHash, targetChainId, targetDenom, onFulfill) {
1702
+ const history = this.getRecentSwapV2History(historyId);
1703
+ if (!history) {
1704
+ onFulfill();
1705
+ return;
1706
+ }
1707
+ const chainInfo = this.chainsService.getChainInfo(targetChainId);
1708
+ if (!chainInfo) {
1709
+ onFulfill();
1710
+ return;
1711
+ }
1712
+ this.trackDestinationAssetAmount({
1713
+ chainId: targetChainId,
1714
+ txHash,
1715
+ recipient: history.recipient,
1716
+ targetDenom,
1717
+ onResult: (resAmount) => {
1718
+ (0, mobx_1.runInAction)(() => {
1719
+ history.resAmount.push(resAmount);
1720
+ history.trackDone = true;
1721
+ this.notifySwapV2Success(history);
1722
+ });
1723
+ },
1724
+ onRefund: (refundInfo, error) => {
1725
+ (0, mobx_1.runInAction)(() => {
1726
+ history.trackError = error;
1727
+ history.assetLocationInfo = Object.assign(Object.assign({}, refundInfo), { type: "refund" });
1728
+ history.trackDone = true;
1729
+ });
1730
+ },
1731
+ onFulfill: () => {
1732
+ (0, mobx_1.runInAction)(() => {
1733
+ history.trackDone = true;
1734
+ });
1735
+ onFulfill();
1736
+ },
1737
+ });
1738
+ }
1739
+ getRecentSwapV2History(id) {
1740
+ return this.recentSwapV2HistoryMap.get(id);
1741
+ }
1742
+ getRecentSwapV2Histories() {
1743
+ return Array.from(this.recentSwapV2HistoryMap.values()).filter((history) => {
1744
+ if (!this.chainsService.hasChainInfo(history.fromChainId)) {
1745
+ return false;
1746
+ }
1747
+ if (!this.chainsService.hasChainInfo(history.toChainId)) {
1748
+ return false;
1749
+ }
1750
+ if (history.simpleRoute.some((route) => {
1751
+ return !this.chainsService.hasChainInfo(route.chainId);
1752
+ })) {
1753
+ return false;
1754
+ }
1755
+ return true;
1756
+ });
1757
+ }
1758
+ removeRecentSwapV2History(id) {
1759
+ const history = this.getRecentSwapV2History(id);
1760
+ const removed = this.recentSwapV2HistoryMap.delete(id);
1761
+ if (removed && (history === null || history === void 0 ? void 0 : history.backgroundExecutionId)) {
1762
+ this.publisher.publish({
1763
+ type: "remove",
1764
+ executionId: history.backgroundExecutionId,
1765
+ });
1766
+ }
1767
+ return removed;
1768
+ }
1769
+ clearAllRecentSwapV2History() {
1770
+ const executionIds = [];
1771
+ for (const history of this.recentSwapV2HistoryMap.values()) {
1772
+ if (history.backgroundExecutionId) {
1773
+ executionIds.push(history.backgroundExecutionId);
1774
+ }
1775
+ }
1776
+ this.recentSwapV2HistoryMap.clear();
1777
+ for (const executionId of executionIds) {
1778
+ this.publisher.publish({
1779
+ type: "remove",
1780
+ executionId,
1781
+ });
1782
+ }
1783
+ }
1784
+ notifySwapV2Success(history) {
1785
+ const notificationInfo = history.notificationInfo;
1786
+ if (!notificationInfo || history.notified) {
1787
+ return;
1788
+ }
1789
+ const latestResAmount = history.resAmount.length > 0
1790
+ ? history.resAmount[history.resAmount.length - 1]
1791
+ : undefined;
1792
+ if (!latestResAmount || latestResAmount.length === 0) {
1793
+ return;
1794
+ }
1795
+ const chainInfo = this.chainsService.getChainInfo(history.toChainId);
1796
+ if (!chainInfo) {
1797
+ return;
1798
+ }
1799
+ const assetsText = latestResAmount
1800
+ .map((amt) => {
1801
+ const currency = notificationInfo.currencies.find((cur) => cur.coinMinimalDenom === amt.denom);
1802
+ if (!currency) {
1803
+ return undefined;
1804
+ }
1805
+ return new unit_1.CoinPretty(currency, amt.amount)
1806
+ .hideIBCMetadata(true)
1807
+ .shrink(true)
1808
+ .maxDecimals(6)
1809
+ .inequalitySymbol(true)
1810
+ .trim(true)
1811
+ .toString();
1812
+ })
1813
+ .filter((text) => Boolean(text));
1814
+ if (assetsText.length === 0) {
1815
+ return;
1816
+ }
1817
+ (0, mobx_1.runInAction)(() => {
1818
+ history.notified = true;
1819
+ });
1820
+ this.notification.create({
1821
+ iconRelativeUrl: "assets/logo-256.png",
1822
+ title: "Swap Succeeded",
1823
+ message: `${assetsText.join(", ")} received on ${chainInfo.chainName}`,
1824
+ });
1825
+ }
1826
+ hideSwapV2History(id) {
1827
+ const history = this.getRecentSwapV2History(id);
1828
+ if (!history) {
1829
+ return false;
1830
+ }
1831
+ if (!history.backgroundExecutionId) {
1832
+ return false;
1833
+ }
1834
+ // only hide if multi tx case
1835
+ history.hidden = true;
1836
+ return true;
1837
+ }
1838
+ showSwapV2History(id) {
1839
+ const history = this.getRecentSwapV2History(id);
1840
+ if (!history) {
1841
+ return false;
1842
+ }
1843
+ history.hidden = false;
1844
+ return true;
1845
+ }
1846
+ setSwapV2HistoryError(id, error) {
1847
+ const history = this.getRecentSwapV2History(id);
1848
+ if (!history) {
1849
+ return false;
1850
+ }
1851
+ history.trackError = error;
1852
+ history.trackDone = true;
1853
+ return true;
1854
+ }
1855
+ clearSwapV2HistoryBackgroundExecutionId(id) {
1856
+ const history = this.getRecentSwapV2History(id);
1857
+ if (!history) {
1858
+ return false;
1859
+ }
1860
+ history.backgroundExecutionId = undefined;
1861
+ return true;
1862
+ }
1863
+ setSwapV2AdditionalTrackingData(id, data) {
1864
+ const history = this.getRecentSwapV2History(id);
1865
+ if (!history) {
1866
+ return false;
1867
+ }
1868
+ if (data.type === "cosmos-ibc") {
1869
+ history.additionalTrackingData = {
1870
+ type: "cosmos-ibc",
1871
+ chainId: data.ibcSwapData.chainId,
1872
+ swapReceiver: data.ibcSwapData.swapReceiver,
1873
+ swapChannelIndex: data.ibcSwapData.swapChannelIndex,
1874
+ txHash: data.txHash,
1875
+ txFulfilled: false,
1876
+ packetTimeout: false,
1877
+ ibcHistory: data.ibcSwapData.ibcChannels.map((ch) => ({
1878
+ portId: ch.portId,
1879
+ channelId: ch.channelId,
1880
+ counterpartyChainId: ch.counterpartyChainId,
1881
+ completed: false,
1882
+ })),
1883
+ };
1884
+ }
1885
+ else {
1886
+ history.additionalTrackingData = data;
1887
+ }
1888
+ history.additionalTrackDone = false;
1889
+ history.additionalTrackError = undefined;
1890
+ this.trackSwapV2AdditionalRecursive(id);
1891
+ return true;
1892
+ }
1893
+ trackSwapV2AdditionalRecursive(id) {
1894
+ const history = this.getRecentSwapV2History(id);
1895
+ if (!history) {
1896
+ return;
1897
+ }
1898
+ // no additional tracking data
1899
+ if (!history.additionalTrackingData) {
1900
+ return;
1901
+ }
1902
+ // already done
1903
+ if (history.additionalTrackDone) {
1904
+ return;
1905
+ }
1906
+ if (history.additionalTrackingData.type === "evm") {
1907
+ this.trackSwapV2AdditionalEVM(id);
1908
+ }
1909
+ else if (history.additionalTrackingData.type === "cosmos-ibc") {
1910
+ this.trackIBCPacketForwardingRecursive((onFulfill, onClose, onError) => {
1911
+ this.trackSwapV2AdditionalCosmosIBCInternal(id, onFulfill, onClose, onError);
1912
+ });
1913
+ }
1914
+ }
1915
+ trackSwapV2AdditionalEVM(id) {
1916
+ var _a;
1917
+ const history = this.getRecentSwapV2History(id);
1918
+ if (!history) {
1919
+ return;
1920
+ }
1921
+ if (((_a = history.additionalTrackingData) === null || _a === void 0 ? void 0 : _a.type) !== "evm") {
1922
+ return;
1923
+ }
1924
+ const txHash = history.additionalTrackingData.txHash;
1925
+ this.traceEVMTransactionResult({
1926
+ chainId: history.toChainId,
1927
+ txHash,
1928
+ recipient: history.recipient,
1929
+ targetDenom: history.destinationAsset.denom,
1930
+ onResult: (result) => {
1931
+ (0, mobx_1.runInAction)(() => {
1932
+ if (result.success && result.resAmount) {
1933
+ history.resAmount.push(result.resAmount);
1934
+ history.assetLocationInfo = undefined;
1935
+ this.notifySwapV2Success(history);
1936
+ }
1937
+ else if (result.refundInfo) {
1938
+ history.additionalTrackError = result.error;
1939
+ history.assetLocationInfo = Object.assign(Object.assign({}, result.refundInfo), { type: "refund" });
1940
+ }
1941
+ history.additionalTrackDone = true;
1942
+ });
1943
+ },
1944
+ onFulfill: () => {
1945
+ (0, mobx_1.runInAction)(() => {
1946
+ history.additionalTrackDone = true;
1947
+ });
1948
+ },
1949
+ });
1950
+ }
1951
+ trackSwapV2AdditionalCosmosIBCInternal(id, onFulfill, onClose, onError) {
1952
+ var _a;
1953
+ const history = this.getRecentSwapV2History(id);
1954
+ if (!history) {
1955
+ onFulfill();
1956
+ return;
1957
+ }
1958
+ if (history.additionalTrackDone) {
1959
+ onFulfill();
1960
+ return;
1961
+ }
1962
+ const existingTrackingData = history.additionalTrackingData;
1963
+ if (!existingTrackingData || existingTrackingData.type !== "cosmos-ibc") {
1964
+ onFulfill();
1965
+ return;
1966
+ }
1967
+ const trackingData = existingTrackingData;
1968
+ const { chainId, txHash, ibcHistory, swapReceiver, txFulfilled } = trackingData;
1969
+ if (!txFulfilled) {
1970
+ this.trackIBCTxFulfillment({
1971
+ chainId,
1972
+ txHash,
1973
+ ibcHistory,
1974
+ swapReceiver,
1975
+ onTxFulfilled: (_tx, firstHopResAmount) => {
1976
+ (0, mobx_1.runInAction)(() => {
1977
+ trackingData.txFulfilled = true;
1978
+ if (firstHopResAmount) {
1979
+ history.resAmount.push(firstHopResAmount);
1980
+ for (let i = history.routeIndex; i < history.simpleRoute.length; i++) {
1981
+ const route = history.simpleRoute[i];
1982
+ if (route.chainId === chainId) {
1983
+ history.routeIndex = i + 1; // move to next route
1984
+ break;
1985
+ }
1986
+ }
1987
+ }
1988
+ });
1989
+ this.trackIBCPacketForwardingRecursive((onFulfill, onClose, onError) => {
1990
+ this.trackSwapV2AdditionalCosmosIBCInternal(id, onFulfill, onClose, onError);
1991
+ });
1992
+ },
1993
+ onTxError: (error) => {
1994
+ (0, mobx_1.runInAction)(() => {
1995
+ history.additionalTrackError = error;
1996
+ history.additionalTrackDone = true;
1997
+ });
1998
+ },
1999
+ onFulfill,
2000
+ onClose,
2001
+ onError,
2002
+ });
2003
+ return;
2004
+ }
2005
+ if (this.handleIbcRewindIfNeeded({
2006
+ sourceChainId: chainId,
2007
+ ibcHistory,
2008
+ packetTimeout: trackingData.packetTimeout,
2009
+ swapContext: ((_a = history.additionalTrackingData) === null || _a === void 0 ? void 0 : _a.type) === "cosmos-ibc"
2010
+ ? {
2011
+ swapReceiver,
2012
+ swapChannelIndex: history.additionalTrackingData.swapChannelIndex,
2013
+ setSwapRefundInfo: (refundInfo) => {
2014
+ (0, mobx_1.runInAction)(() => {
2015
+ history.assetLocationInfo = Object.assign(Object.assign({}, refundInfo), { type: "refund" });
2016
+ });
2017
+ },
2018
+ }
2019
+ : undefined,
2020
+ onFulfill,
2021
+ onClose,
2022
+ onError,
2023
+ onRewindComplete: () => {
2024
+ // rewound되었다면 실패 처리하는 것이 맞겠지...
2025
+ (0, mobx_1.runInAction)(() => {
2026
+ var _a;
2027
+ history.status = types_1.SwapV2TxStatus.FAILED;
2028
+ history.additionalTrackDone = true;
2029
+ const rewoundChainId = (_a = ibcHistory.find((h) => h.rewound)) === null || _a === void 0 ? void 0 : _a.counterpartyChainId;
2030
+ if (rewoundChainId) {
2031
+ for (let i = history.simpleRoute.length - 1; i >= 0; i--) {
2032
+ const route = history.simpleRoute[i];
2033
+ if (route.chainId === rewoundChainId) {
2034
+ history.routeIndex = i; // 실패한 채널 인덱스로 이동
2035
+ break;
2036
+ }
2037
+ }
2038
+ }
2039
+ });
2040
+ this.trackIBCPacketForwardingRecursive((onFulfill, onClose, onError) => {
2041
+ this.trackSwapV2AdditionalCosmosIBCInternal(id, onFulfill, onClose, onError);
2042
+ });
2043
+ },
2044
+ })) {
2045
+ return;
2046
+ }
2047
+ this.trackIbcHopFlowWithTimeout({
2048
+ ibcHistory,
2049
+ sourceChainId: chainId,
2050
+ swapReceiver,
2051
+ destinationAsset: history.destinationAsset,
2052
+ onHopCompleted: (resAmount) => {
2053
+ (0, mobx_1.runInAction)(() => {
2054
+ if (resAmount) {
2055
+ history.resAmount.push(resAmount);
2056
+ }
2057
+ });
2058
+ },
2059
+ onAllCompleted: () => {
2060
+ (0, mobx_1.runInAction)(() => {
2061
+ history.additionalTrackDone = true;
2062
+ history.assetLocationInfo = undefined;
2063
+ // Update routeIndex to the last index to show completion state in UI
2064
+ history.routeIndex = history.simpleRoute.length;
2065
+ this.notifySwapV2Success(history);
2066
+ });
2067
+ },
2068
+ onContinue: () => {
2069
+ this.trackIBCPacketForwardingRecursive((onFulfill, onClose, onError) => {
2070
+ this.trackSwapV2AdditionalCosmosIBCInternal(id, onFulfill, onClose, onError);
2071
+ });
2072
+ },
2073
+ onRetry: () => {
2074
+ this.trackIBCPacketForwardingRecursive((onFulfill, onClose, onError) => {
2075
+ this.trackSwapV2AdditionalCosmosIBCInternal(id, onFulfill, onClose, onError);
2076
+ });
2077
+ },
2078
+ onPacketTimeout: () => {
2079
+ (0, mobx_1.runInAction)(() => {
2080
+ trackingData.packetTimeout = true;
2081
+ });
2082
+ },
2083
+ onDynamicHopDetected: () => {
2084
+ (0, mobx_1.runInAction)(() => {
2085
+ trackingData.dynamicHopDetected = true;
2086
+ });
2087
+ },
2088
+ onFulfill,
2089
+ onClose,
2090
+ onError,
2091
+ });
2092
+ }
2093
+ // ============================================================================
2094
+ // IBC Packet Tracking Core Functions
2095
+ // ============================================================================
2096
+ /**
2097
+ * IBC tx 완료 대기 및 첫 번째 hop res amount 추출
2098
+ */
2099
+ trackIBCTxFulfillment(params) {
2100
+ const { chainId, txHash, ibcHistory, swapReceiver, onTxFulfilled, onTxError, onFulfill, onClose, onError, } = params;
2101
+ const chainInfo = this.chainsService.getChainInfo(chainId);
2102
+ if (!chainInfo) {
2103
+ onFulfill();
2104
+ return;
2105
+ }
2106
+ const txTracer = new cosmos_1.TendermintTxTracer(chainInfo.rpc, "/websocket");
2107
+ txTracer.addEventListener("close", onClose);
2108
+ txTracer.addEventListener("error", onError);
2109
+ txTracer.traceTx(buffer_1.Buffer.from(txHash, "hex")).then((tx) => {
2110
+ txTracer.close();
2111
+ if (tx.code != null && tx.code !== 0) {
2112
+ onTxError(tx.log || tx.raw_log || "Tx failed");
2113
+ onFulfill();
2114
+ return;
1235
2115
  }
1236
- if (!this.chainsService.hasChainInfo(history.destinationChainId)) {
1237
- return false;
2116
+ let resAmount;
2117
+ if (swapReceiver && swapReceiver.length > 0) {
2118
+ resAmount = this.getIBCSwapResAmountFromTx(tx, swapReceiver[0]);
1238
2119
  }
1239
- if (history.simpleRoute.some((route) => {
1240
- return !this.chainsService.hasChainInfo(route.chainId);
1241
- })) {
1242
- return false;
2120
+ if (ibcHistory.length > 0) {
2121
+ const firstChannel = ibcHistory[0];
2122
+ firstChannel.sequence = this.getIBCPacketSequenceFromTx(tx, firstChannel.portId, firstChannel.channelId);
2123
+ firstChannel.dstChannelId = this.getDstChannelIdFromTx(tx, firstChannel.portId, firstChannel.channelId);
1243
2124
  }
1244
- return true;
2125
+ onTxFulfilled(tx, resAmount);
2126
+ onFulfill();
1245
2127
  });
1246
2128
  }
1247
- trackSkipSwapRecursive(id) {
1248
- const history = this.getRecentSkipHistory(id);
1249
- if (!history) {
1250
- return;
2129
+ /**
2130
+ * IBC rewind 필요 여부 확인 및 rewind 처리
2131
+ */
2132
+ handleIbcRewindIfNeeded(params) {
2133
+ const { sourceChainId, ibcHistory, packetTimeout, swapContext, onFulfill, onClose, onError, onRewindComplete, } = params;
2134
+ if (ibcHistory.length === 0) {
2135
+ return false;
1251
2136
  }
1252
- // check tx fulfilled and update history
1253
- (0, common_1.retry)(() => {
1254
- return new Promise((txFulfilledResolve, txFulfilledReject) => {
1255
- this.checkAndTrackSkipSwapTxFulfilledRecursive(history, (keepTracking) => {
1256
- txFulfilledResolve();
1257
- if (!keepTracking) {
2137
+ const needRewind = ibcHistory.find((h) => h.error != null) != null;
2138
+ if (!needRewind) {
2139
+ return false;
2140
+ }
2141
+ if (ibcHistory.find((h) => h.rewoundButNextRewindingBlocked)) {
2142
+ onFulfill();
2143
+ return true;
2144
+ }
2145
+ const isTimeoutPacket = packetTimeout !== null && packetTimeout !== void 0 ? packetTimeout : false;
2146
+ const lastRewoundChannelIndex = ibcHistory.findIndex((h) => {
2147
+ if (h.rewound) {
2148
+ return true;
2149
+ }
2150
+ });
2151
+ const targetChannel = (() => {
2152
+ if (lastRewoundChannelIndex >= 0) {
2153
+ if (lastRewoundChannelIndex === 0) {
2154
+ return undefined;
2155
+ }
2156
+ return ibcHistory[lastRewoundChannelIndex - 1];
2157
+ }
2158
+ return ibcHistory.find((h) => h.error != null);
2159
+ })();
2160
+ const isSwapTargetChannel = !!(targetChannel &&
2161
+ swapContext &&
2162
+ ibcHistory.indexOf(targetChannel) === swapContext.swapChannelIndex + 1);
2163
+ if (targetChannel !== undefined && targetChannel.sequence !== undefined) {
2164
+ const targetSequence = targetChannel.sequence;
2165
+ const prevChainInfo = (() => {
2166
+ const targetChannelIndex = ibcHistory.findIndex((h) => h === targetChannel);
2167
+ if (targetChannelIndex < 0) {
2168
+ return undefined;
2169
+ }
2170
+ if (targetChannelIndex === 0) {
2171
+ return this.chainsService.getChainInfo(sourceChainId);
2172
+ }
2173
+ return this.chainsService.getChainInfo(ibcHistory[targetChannelIndex - 1].counterpartyChainId);
2174
+ })();
2175
+ if (prevChainInfo) {
2176
+ const txTracer = new cosmos_1.TendermintTxTracer(prevChainInfo.rpc, "/websocket");
2177
+ txTracer.addEventListener("close", onClose);
2178
+ txTracer.addEventListener("error", onError);
2179
+ txTracer
2180
+ .traceTx(isTimeoutPacket
2181
+ ? {
2182
+ // "timeout_packet.packet_src_port": targetChannel.portId,
2183
+ "timeout_packet.packet_src_channel": targetChannel.channelId,
2184
+ "timeout_packet.packet_sequence": targetChannel.sequence,
2185
+ }
2186
+ : {
2187
+ // "acknowledge_packet.packet_src_port": targetChannel.portId,
2188
+ "acknowledge_packet.packet_src_channel": targetChannel.channelId,
2189
+ "acknowledge_packet.packet_sequence": targetChannel.sequence,
2190
+ })
2191
+ .then((res) => {
2192
+ txTracer.close();
2193
+ if (!res) {
1258
2194
  return;
1259
2195
  }
1260
- (0, common_1.retry)(() => {
1261
- return new Promise((resolve, reject) => {
1262
- this.checkAndUpdateSkipSwapHistoryRecursive(id, resolve, reject);
1263
- });
1264
- }, {
1265
- maxRetries: 50,
1266
- waitMsAfterError: 500,
1267
- maxWaitMsAfterError: 15000,
2196
+ (0, mobx_1.runInAction)(() => {
2197
+ var _a;
2198
+ if (isSwapTargetChannel) {
2199
+ const txs = res.txs
2200
+ ? res.txs.map((res) => res.tx_result || res)
2201
+ : [res.tx_result || res];
2202
+ if (txs && Array.isArray(txs)) {
2203
+ for (const tx of txs) {
2204
+ const index = isTimeoutPacket
2205
+ ? this.getIBCTimeoutPacketIndexFromTx(tx, targetChannel.portId, targetChannel.channelId, targetSequence)
2206
+ : this.getIBCAcknowledgementPacketIndexFromTx(tx, targetChannel.portId, targetChannel.channelId, targetSequence);
2207
+ if (index >= 0) {
2208
+ // 좀 빡치게 timeout packet은 refund 로직이 실행되고 나서 "timeout_packet" event가 발생한다.
2209
+ const refunded = isTimeoutPacket
2210
+ ? this.getIBCSwapResAmountFromTx(tx, swapContext.swapReceiver[swapContext.swapChannelIndex + 1], (() => {
2211
+ const i = this.getLastIBCTimeoutPacketBeforeIndexFromTx(tx, index);
2212
+ if (i < 0) {
2213
+ return 0;
2214
+ }
2215
+ return i;
2216
+ })(), index)
2217
+ : this.getIBCSwapResAmountFromTx(tx, swapContext.swapReceiver[swapContext.swapChannelIndex + 1], index);
2218
+ (_a = swapContext.setSwapRefundInfo) === null || _a === void 0 ? void 0 : _a.call(swapContext, {
2219
+ chainId: prevChainInfo.chainId,
2220
+ amount: refunded,
2221
+ });
2222
+ targetChannel.rewoundButNextRewindingBlocked = true;
2223
+ break;
2224
+ }
2225
+ }
2226
+ }
2227
+ }
2228
+ targetChannel.rewound = true;
1268
2229
  });
1269
- }, txFulfilledReject);
1270
- });
1271
- }, {
1272
- maxRetries: 50,
1273
- waitMsAfterError: 500,
1274
- maxWaitMsAfterError: 15000,
1275
- });
1276
- }
1277
- trackDestinationAssetAmount(historyId, txHash, onFulfill) {
1278
- const history = this.getRecentSkipHistory(historyId);
1279
- if (!history) {
1280
- onFulfill();
1281
- return;
2230
+ onFulfill();
2231
+ onRewindComplete();
2232
+ });
2233
+ }
1282
2234
  }
1283
- const chainInfo = this.chainsService.getChainInfo(history.destinationChainId);
1284
- if (!chainInfo) {
2235
+ return true;
2236
+ }
2237
+ trackIbcHopFlowWithTimeout(params) {
2238
+ const { ibcHistory, sourceChainId, swapReceiver, destinationAsset, onHopCompleted, onAllCompleted, onContinue, onRetry, onPacketTimeout, onDynamicHopDetected, onFulfill, onClose, onError, } = params;
2239
+ const targetChannelIndex = ibcHistory.findIndex((history) => {
2240
+ return !history.completed;
2241
+ });
2242
+ const targetChannel = ibcHistory[targetChannelIndex];
2243
+ if (!targetChannel || !targetChannel.sequence) {
1285
2244
  onFulfill();
1286
2245
  return;
1287
2246
  }
1288
- if (this.chainsService.isEvmChain(history.destinationChainId)) {
1289
- const evmInfo = chainInfo.evm;
1290
- if (!evmInfo) {
2247
+ const closables = [];
2248
+ const closeClosables = () => {
2249
+ closables.forEach((closable) => {
2250
+ if (closable.readyState === cosmos_1.WsReadyState.OPEN ||
2251
+ closable.readyState === cosmos_1.WsReadyState.CONNECTING) {
2252
+ closable.close();
2253
+ }
2254
+ });
2255
+ };
2256
+ let _onFulfillOnce = false;
2257
+ const onFulfillOnce = () => {
2258
+ if (!_onFulfillOnce) {
2259
+ _onFulfillOnce = true;
2260
+ closeClosables();
1291
2261
  onFulfill();
1292
- return;
1293
2262
  }
1294
- (0, simple_fetch_1.simpleFetch)(evmInfo.rpc, {
1295
- method: "POST",
1296
- headers: {
1297
- "content-type": "application/json",
1298
- "request-source": origin,
1299
- },
1300
- body: JSON.stringify({
1301
- jsonrpc: "2.0",
1302
- method: "eth_getTransactionReceipt",
1303
- params: [txHash],
1304
- id: 1,
1305
- }),
1306
- })
1307
- .then((res) => {
1308
- const txReceipt = res.data.result;
1309
- if (txReceipt) {
1310
- (0, simple_fetch_1.simpleFetch)(evmInfo.rpc, {
1311
- method: "POST",
1312
- headers: {
1313
- "content-type": "application/json",
1314
- "request-source": origin,
1315
- },
1316
- body: JSON.stringify({
1317
- jsonrpc: "2.0",
1318
- method: "debug_traceTransaction",
1319
- params: [txHash, { tracer: "callTracer" }],
1320
- id: 1,
1321
- }),
1322
- }).then((res) => {
1323
- (0, mobx_1.runInAction)(() => {
1324
- let isFoundFromCall = false;
1325
- if (res.data.result) {
1326
- const searchForTransfers = (calls) => {
1327
- for (const call of calls) {
1328
- if (call.type === "CALL" &&
1329
- call.to.toLowerCase() ===
1330
- history.recipient.toLowerCase()) {
1331
- const isERC20Transfer = call.input.startsWith("0xa9059cbb");
1332
- const value = BigInt(isERC20Transfer
1333
- ? `0x${call.input.substring(74)}`
1334
- : call.value);
1335
- history.resAmount.push([
1336
- {
1337
- amount: value.toString(10),
1338
- denom: history.destinationAsset.denom,
1339
- },
1340
- ]);
1341
- isFoundFromCall = true;
1342
- }
1343
- if (call.calls && call.calls.length > 0) {
1344
- searchForTransfers(call.calls);
1345
- }
1346
- }
1347
- };
1348
- searchForTransfers(res.data.result.calls || []);
1349
- }
1350
- if (isFoundFromCall) {
1351
- history.trackDone = true;
1352
- return;
1353
- }
1354
- const logs = txReceipt.logs;
1355
- const transferTopic = (0, hash_1.id)("Transfer(address,address,uint256)");
1356
- const withdrawTopic = (0, hash_1.id)("Withdrawal(address,uint256)");
1357
- const hyperlaneReceiveTopic = (0, hash_1.id)("ReceivedTransferRemote(uint32,bytes32,uint256)");
1358
- for (const log of logs) {
1359
- if (log.topics[0] === transferTopic) {
1360
- const to = "0x" + log.topics[2].slice(26);
1361
- if (to.toLowerCase() === history.recipient.toLowerCase()) {
1362
- const destinationAssetDenom = history.destinationAsset.denom.replace("erc20:", "");
1363
- const amount = BigInt(log.data).toString(10);
1364
- if (log.address === destinationAssetDenom) {
1365
- history.resAmount.push([
1366
- {
1367
- amount,
1368
- denom: history.destinationAsset.denom,
1369
- },
1370
- ]);
1371
- }
1372
- else {
1373
- console.log("refunded", log.address);
1374
- // Transfer 토픽인 경우엔 ERC20의 tranfer 호출일텐데
1375
- // 받을 토큰의 컨트랙트가 아닌 다른 컨트랙트에서 호출된 경우는 Swap을 실패한 것으로 추측
1376
- // 고로 실제로 받은 토큰의 컨트랙트 주소로 환불 정보에 저장한다.
1377
- history.trackError = "Swap failed";
1378
- history.swapRefundInfo = {
1379
- chainId: history.destinationChainId,
1380
- amount: [
1381
- {
1382
- amount,
1383
- denom: `erc20:${log.address.toLowerCase()}`,
1384
- },
1385
- ],
1386
- };
1387
- }
1388
- history.trackDone = true;
1389
- return;
1390
- }
1391
- }
1392
- else if (log.topics[0] === withdrawTopic) {
1393
- const to = "0x" + log.topics[1].slice(26);
1394
- if (to.toLowerCase() === txReceipt.to.toLowerCase()) {
1395
- const amount = BigInt(log.data).toString(10);
1396
- history.resAmount.push([
1397
- { amount, denom: history.destinationAsset.denom },
1398
- ]);
1399
- history.trackDone = true;
1400
- return;
1401
- }
1402
- }
1403
- else if (log.topics[0] === hyperlaneReceiveTopic) {
1404
- const to = "0x" + log.topics[2].slice(26);
1405
- if (to.toLowerCase() === history.recipient.toLowerCase()) {
1406
- const amount = BigInt(log.data).toString(10);
1407
- // Hyperlane을 통해 Forma로 TIA를 받는 경우 토큰 수량이 decimal 6으로 기록되는데,
1408
- // Forma에서는 decimal 18이기 때문에 12자리 만큼 0을 붙여준다.
1409
- history.resAmount.push([
1410
- {
1411
- amount: history.destinationAsset.denom === "forma-native"
1412
- ? `${amount}000000000000`
1413
- : amount,
1414
- denom: history.destinationAsset.denom,
1415
- },
1416
- ]);
1417
- history.trackDone = true;
1418
- return;
1419
- }
2263
+ };
2264
+ let _onCloseOnce = false;
2265
+ const onCloseOnce = () => {
2266
+ if (!_onCloseOnce) {
2267
+ _onCloseOnce = true;
2268
+ closeClosables();
2269
+ onClose();
2270
+ }
2271
+ };
2272
+ let _onErrorOnce = false;
2273
+ const onErrorOnce = () => {
2274
+ if (!_onErrorOnce) {
2275
+ _onErrorOnce = true;
2276
+ closeClosables();
2277
+ onError();
2278
+ }
2279
+ };
2280
+ const registerClosable = (closable) => closables.push(closable);
2281
+ let hopFailed = false;
2282
+ const hopTracer = this.trackIBCHopRecvPacket({
2283
+ ibcHistory,
2284
+ targetChannelIndex,
2285
+ swapReceiver,
2286
+ destinationAsset,
2287
+ onHopCompleted: (resAmount, tx) => {
2288
+ onHopCompleted === null || onHopCompleted === void 0 ? void 0 : onHopCompleted(resAmount, tx);
2289
+ const sequence = targetChannel.sequence;
2290
+ if (!sequence) {
2291
+ hopFailed = true;
2292
+ onFulfillOnce();
2293
+ return;
2294
+ }
2295
+ if (!tx) {
2296
+ return;
2297
+ }
2298
+ const txs = tx.txs
2299
+ ? tx.txs.map((res) => res.tx_result || res)
2300
+ : [tx.tx_result || tx];
2301
+ for (const txItem of txs) {
2302
+ try {
2303
+ const ack = this.getIBCWriteAcknowledgementAckFromTx(txItem, targetChannel.portId, targetChannel.channelId, sequence);
2304
+ if (ack && ack.length > 0) {
2305
+ const str = buffer_1.Buffer.from(ack);
2306
+ try {
2307
+ const decoded = JSON.parse(str.toString());
2308
+ if (decoded.error) {
2309
+ (0, mobx_1.runInAction)(() => {
2310
+ targetChannel.error = "Packet processing failed";
2311
+ });
2312
+ hopFailed = true;
2313
+ onFulfillOnce();
2314
+ onRetry();
2315
+ return;
1420
2316
  }
1421
2317
  }
1422
- });
1423
- });
2318
+ catch (e) {
2319
+ console.log(e);
2320
+ }
2321
+ }
2322
+ }
2323
+ catch (_a) {
2324
+ // noop
2325
+ }
2326
+ const index = this.getIBCRecvPacketIndexFromTx(txItem, targetChannel.portId, targetChannel.channelId, sequence);
2327
+ if (index >= 0) {
2328
+ break;
2329
+ }
1424
2330
  }
1425
- })
1426
- .finally(() => {
1427
- history.trackDone = true;
1428
- onFulfill();
1429
- });
1430
- }
1431
- else {
1432
- const txTracer = new cosmos_1.TendermintTxTracer(chainInfo.rpc, "/websocket");
1433
- txTracer.addEventListener("error", () => onFulfill());
1434
- txTracer
1435
- .queryTx({
1436
- "tx.hash": txHash,
1437
- })
1438
- .then((res) => {
1439
- txTracer.close();
1440
- if (!res) {
2331
+ },
2332
+ onAllCompleted: () => {
2333
+ if (hopFailed) {
2334
+ return;
2335
+ }
2336
+ onAllCompleted();
2337
+ onFulfillOnce();
2338
+ },
2339
+ onContinue: () => {
2340
+ if (hopFailed) {
2341
+ return;
2342
+ }
2343
+ onContinue();
2344
+ onFulfillOnce();
2345
+ },
2346
+ onFulfill: onFulfillOnce,
2347
+ onClose: onCloseOnce,
2348
+ onError: onErrorOnce,
2349
+ onDynamicHopDetected: (hopInfo) => {
2350
+ if (hopFailed) {
1441
2351
  return;
1442
2352
  }
2353
+ // 동적으로 감지된 새 홉을 ibcHistory에 추가한다.
1443
2354
  (0, mobx_1.runInAction)(() => {
1444
- const txs = res.txs
1445
- ? res.txs.map((res) => res.tx_result || res)
1446
- : [res.tx_result || res];
1447
- for (const tx of txs) {
1448
- const resAmount = this.getIBCSwapResAmountFromTx(tx, history.recipient);
1449
- history.resAmount.push(resAmount);
1450
- history.trackDone = true;
2355
+ ibcHistory.push({
2356
+ portId: hopInfo.portId,
2357
+ channelId: hopInfo.channelId,
2358
+ counterpartyChainId: hopInfo.counterpartyChainId,
2359
+ sequence: hopInfo.sequence,
2360
+ dstChannelId: hopInfo.dstChannelId,
2361
+ completed: false,
2362
+ });
2363
+ if (swapReceiver && hopInfo.packetData.receiver) {
2364
+ swapReceiver.push(hopInfo.packetData.receiver);
2365
+ }
2366
+ });
2367
+ onDynamicHopDetected === null || onDynamicHopDetected === void 0 ? void 0 : onDynamicHopDetected();
2368
+ onContinue();
2369
+ onFulfillOnce();
2370
+ },
2371
+ });
2372
+ if (hopTracer) {
2373
+ registerClosable(hopTracer);
2374
+ }
2375
+ let prevChainId;
2376
+ if (targetChannelIndex > 0) {
2377
+ prevChainId = ibcHistory[targetChannelIndex - 1].counterpartyChainId;
2378
+ }
2379
+ else {
2380
+ prevChainId = sourceChainId;
2381
+ }
2382
+ if (prevChainId) {
2383
+ const prevChainInfo = this.chainsService.getChainInfo(prevChainId);
2384
+ if (prevChainInfo) {
2385
+ const queryEvents = {
2386
+ // acknowledge_packet과는 다르게 timeout_packet은 이전의 체인의 이벤트로부터만 알 수 있다.
2387
+ // 방법이 없기 때문에 여기서 이전의 체인으로부터 subscribe를 해서 이벤트를 받아야 한다.
2388
+ // 하지만 이 경우 ibc error tracking 로직에서 이것과 똑같은 subscription을 한번 더 하게 된다.
2389
+ // 이미 로직이 많이 복잡하기 때문에 로직을 덜 복잡하게 하기 위해서 이러한 비효율성(?)을 감수한다.
2390
+ // "timeout_packet.packet_src_port": targetChannel.portId,
2391
+ "timeout_packet.packet_src_channel": targetChannel.channelId,
2392
+ "timeout_packet.packet_sequence": targetChannel.sequence,
2393
+ };
2394
+ const txTracer = new cosmos_1.TendermintTxTracer(prevChainInfo.rpc, "/websocket");
2395
+ registerClosable(txTracer);
2396
+ txTracer.addEventListener("close", onCloseOnce);
2397
+ txTracer.addEventListener("error", onErrorOnce);
2398
+ txTracer.traceTx(queryEvents).then((res) => {
2399
+ txTracer.close();
2400
+ if (!res) {
1451
2401
  return;
1452
2402
  }
2403
+ (0, mobx_1.runInAction)(() => {
2404
+ targetChannel.error = "Packet timeout";
2405
+ onPacketTimeout === null || onPacketTimeout === void 0 ? void 0 : onPacketTimeout();
2406
+ hopFailed = true;
2407
+ onFulfillOnce();
2408
+ onRetry();
2409
+ });
1453
2410
  });
1454
- })
1455
- .finally(() => {
1456
- history.trackDone = true;
1457
- onFulfill();
1458
- });
2411
+ }
1459
2412
  }
1460
2413
  }
1461
- removeRecentSkipHistory(id) {
1462
- return this.recentSkipHistoryMap.delete(id);
1463
- }
1464
- clearAllRecentSkipHistory() {
1465
- this.recentSkipHistoryMap.clear();
2414
+ trackIBCHopRecvPacket(params) {
2415
+ const { ibcHistory, targetChannelIndex, swapReceiver, destinationAsset, onHopCompleted, onAllCompleted, onContinue, onFulfill, onClose, onError, onDynamicHopDetected, } = params;
2416
+ const targetChannel = ibcHistory[targetChannelIndex];
2417
+ const nextChannel = targetChannelIndex + 1 < ibcHistory.length
2418
+ ? ibcHistory[targetChannelIndex + 1]
2419
+ : undefined;
2420
+ if (!targetChannel || !targetChannel.sequence) {
2421
+ onAllCompleted();
2422
+ return;
2423
+ }
2424
+ const sequence = targetChannel.sequence;
2425
+ const counterpartyChainInfo = this.chainsService.getChainInfo(targetChannel.counterpartyChainId);
2426
+ if (!counterpartyChainInfo) {
2427
+ onFulfill();
2428
+ return;
2429
+ }
2430
+ const queryEvents = {
2431
+ // "recv_packet.packet_src_port": targetChannel.portId,
2432
+ "recv_packet.packet_dst_channel": targetChannel.dstChannelId,
2433
+ "recv_packet.packet_sequence": targetChannel.sequence,
2434
+ };
2435
+ const txTracer = new cosmos_1.TendermintTxTracer(counterpartyChainInfo.rpc, "/websocket");
2436
+ txTracer.addEventListener("close", onClose);
2437
+ txTracer.addEventListener("error", onError);
2438
+ txTracer.traceTx(queryEvents).then((res) => {
2439
+ txTracer.close();
2440
+ if (!res) {
2441
+ onError();
2442
+ return;
2443
+ }
2444
+ const txs = res.txs
2445
+ ? res.txs.map((t) => t.tx_result || t)
2446
+ : [res.tx_result || res];
2447
+ const matchedTx = txs.find((tx) => {
2448
+ const idx = this.getIBCRecvPacketIndexFromTx(tx, targetChannel.portId, targetChannel.channelId, sequence);
2449
+ return idx >= 0;
2450
+ });
2451
+ const tx = matchedTx || txs[0];
2452
+ targetChannel.completed = true;
2453
+ let resAmount;
2454
+ const receiverIndex = targetChannelIndex + 1;
2455
+ if (tx && swapReceiver && receiverIndex < swapReceiver.length) {
2456
+ const index = this.getIBCRecvPacketIndexFromTx(tx, targetChannel.portId, targetChannel.channelId, sequence);
2457
+ if (index >= 0) {
2458
+ resAmount = this.getIBCSwapResAmountFromTx(tx, swapReceiver[receiverIndex], index, undefined);
2459
+ }
2460
+ }
2461
+ onHopCompleted(resAmount, tx);
2462
+ // 1. 다음 채널이 있고, tx가 있는 경우 => 다음 채널로 이동
2463
+ // 2. 마지막 채널이면서, destinationAsset가 있고, onDynamicHopDetected가 있는 경우 => 동적 홉 감지
2464
+ // 3. 그 외의 경우 => 종료(allCompleted)
2465
+ if (nextChannel && tx) {
2466
+ const index = this.getIBCRecvPacketIndexFromTx(tx, targetChannel.portId, targetChannel.channelId, sequence);
2467
+ if (index >= 0) {
2468
+ nextChannel.sequence = this.getIBCPacketSequenceFromTx(tx, nextChannel.portId, nextChannel.channelId, index);
2469
+ nextChannel.dstChannelId = this.getDstChannelIdFromTx(tx, nextChannel.portId, nextChannel.channelId, index);
2470
+ }
2471
+ onContinue();
2472
+ }
2473
+ else if (tx && destinationAsset && onDynamicHopDetected) {
2474
+ // 마지막 채널에서 recv_packet 이후 새로운 send_packet이 있는 경우를 감지
2475
+ const index = this.getIBCRecvPacketIndexFromTx(tx, targetChannel.portId, targetChannel.channelId, sequence);
2476
+ if (index >= 0) {
2477
+ const sendPackets = this.getSendPacketInfoFromTx(tx, index);
2478
+ if (sendPackets.length > 0) {
2479
+ // 마지막 send_packet을 확인
2480
+ const lastSendPacket = sendPackets[sendPackets.length - 1];
2481
+ // destination chain에서 IBC wrapped token을 unwrap하는 경우를 가정한다.
2482
+ // 예를 들어, source chain -> osmosis (swap) -> destination chain으로 이동하는 경우,
2483
+ // osmosis에서 IBC wrapped token이 destination chain으로 전송되고,
2484
+ // destination chain에서 IBC wrapped token을 unwrap하는 케이스가 있을 수 있다.
2485
+ const destinationChainInfo = this.chainsService.getChainInfo(destinationAsset.chainId);
2486
+ if (destinationChainInfo) {
2487
+ onDynamicHopDetected({
2488
+ portId: "transfer",
2489
+ channelId: lastSendPacket.srcChannel,
2490
+ counterpartyChainId: destinationChainInfo.chainId,
2491
+ sequence: lastSendPacket.sequence,
2492
+ dstChannelId: lastSendPacket.dstChannel,
2493
+ packetData: lastSendPacket.packetData,
2494
+ });
2495
+ return;
2496
+ }
2497
+ }
2498
+ }
2499
+ onAllCompleted();
2500
+ }
2501
+ else {
2502
+ onAllCompleted();
2503
+ }
2504
+ });
2505
+ return txTracer;
1466
2506
  }
1467
2507
  getIBCWriteAcknowledgementAckFromTx(tx, sourcePortId, sourceChannelId, sequence) {
1468
2508
  const events = tx.events;
@@ -1597,6 +2637,92 @@ class RecentSendHistoryService {
1597
2637
  }
1598
2638
  return [];
1599
2639
  }
2640
+ /**
2641
+ * TX에서 send_packet 이벤트 정보를 추출하여 추가 IBC 홉을 감지하기 위해 사용됩니다.
2642
+ *
2643
+ * 예를 들어, 마지막 목적지 체인에서 IBC wrapped token을 unwrap하는 경우,
2644
+ * send_packet 이벤트를 통해 추가 IBC 홉을 감지할 수 있습니다.
2645
+ */
2646
+ getSendPacketInfoFromTx(tx, startEventsIndex = 0) {
2647
+ var _a, _b, _c, _d;
2648
+ const events = tx.events;
2649
+ if (!events || !Array.isArray(events)) {
2650
+ return [];
2651
+ }
2652
+ const compareStringWithBase64OrPlain = (target, value) => {
2653
+ if (target === value) {
2654
+ return [true, false];
2655
+ }
2656
+ if (target === buffer_1.Buffer.from(value).toString("base64")) {
2657
+ return [true, true];
2658
+ }
2659
+ return [false, false];
2660
+ };
2661
+ const results = [];
2662
+ const slicedEvents = events.slice(startEventsIndex);
2663
+ for (const event of slicedEvents) {
2664
+ if (event.type !== "send_packet") {
2665
+ continue;
2666
+ }
2667
+ let isBase64 = false;
2668
+ const srcChannelAttr = (_a = event.attributes) === null || _a === void 0 ? void 0 : _a.find((attr) => {
2669
+ const c = compareStringWithBase64OrPlain(attr.key, "packet_src_channel");
2670
+ isBase64 = c[1];
2671
+ return c[0];
2672
+ });
2673
+ if (!srcChannelAttr) {
2674
+ continue;
2675
+ }
2676
+ const dstChannelAttr = (_b = event.attributes) === null || _b === void 0 ? void 0 : _b.find((attr) => {
2677
+ return compareStringWithBase64OrPlain(attr.key, "packet_dst_channel")[0];
2678
+ });
2679
+ if (!dstChannelAttr)
2680
+ continue;
2681
+ const sequenceAttr = (_c = event.attributes) === null || _c === void 0 ? void 0 : _c.find((attr) => {
2682
+ return compareStringWithBase64OrPlain(attr.key, "packet_sequence")[0];
2683
+ });
2684
+ if (!sequenceAttr)
2685
+ continue;
2686
+ const packetDataAttr = (_d = event.attributes) === null || _d === void 0 ? void 0 : _d.find((attr) => {
2687
+ return compareStringWithBase64OrPlain(attr.key, "packet_data")[0];
2688
+ });
2689
+ if (!packetDataAttr)
2690
+ continue;
2691
+ try {
2692
+ const srcChannel = isBase64
2693
+ ? buffer_1.Buffer.from(srcChannelAttr.value, "base64").toString()
2694
+ : srcChannelAttr.value;
2695
+ const dstChannel = isBase64
2696
+ ? buffer_1.Buffer.from(dstChannelAttr.value, "base64").toString()
2697
+ : dstChannelAttr.value;
2698
+ const sequence = isBase64
2699
+ ? buffer_1.Buffer.from(sequenceAttr.value, "base64").toString()
2700
+ : sequenceAttr.value;
2701
+ const packetDataValue = isBase64
2702
+ ? buffer_1.Buffer.from(packetDataAttr.value, "base64").toString()
2703
+ : packetDataAttr.value.startsWith("{")
2704
+ ? packetDataAttr.value
2705
+ : buffer_1.Buffer.from(packetDataAttr.value, "base64").toString();
2706
+ const packetData = JSON.parse(packetDataValue);
2707
+ if (packetData.denom && packetData.amount && packetData.receiver) {
2708
+ results.push({
2709
+ srcChannel,
2710
+ dstChannel,
2711
+ sequence,
2712
+ packetData: {
2713
+ denom: packetData.denom,
2714
+ amount: packetData.amount,
2715
+ receiver: packetData.receiver,
2716
+ },
2717
+ });
2718
+ }
2719
+ }
2720
+ catch (_e) {
2721
+ // noop
2722
+ }
2723
+ }
2724
+ return results;
2725
+ }
1600
2726
  getIBCAcknowledgementPacketIndexFromTx(tx, sourcePortId, sourceChannelId, sequence) {
1601
2727
  const events = tx.events;
1602
2728
  if (!events) {
@@ -1942,6 +3068,129 @@ class RecentSendHistoryService {
1942
3068
  }
1943
3069
  throw new Error("Invalid tx");
1944
3070
  }
3071
+ removeIBCHistoriesByChainIdentifier(chainIdentifier) {
3072
+ const removingIds = [];
3073
+ for (const history of this.recentIBCHistoryMap.values()) {
3074
+ if (cosmos_1.ChainIdHelper.parse(history.chainId).identifier === chainIdentifier) {
3075
+ removingIds.push(history.id);
3076
+ continue;
3077
+ }
3078
+ if (cosmos_1.ChainIdHelper.parse(history.destinationChainId).identifier ===
3079
+ chainIdentifier) {
3080
+ removingIds.push(history.id);
3081
+ continue;
3082
+ }
3083
+ if (history.ibcHistory.some((history) => {
3084
+ return (cosmos_1.ChainIdHelper.parse(history.counterpartyChainId).identifier ===
3085
+ chainIdentifier);
3086
+ })) {
3087
+ removingIds.push(history.id);
3088
+ continue;
3089
+ }
3090
+ }
3091
+ for (const id of removingIds) {
3092
+ this.recentIBCHistoryMap.delete(id);
3093
+ }
3094
+ }
3095
+ removeSkipHistoriesByChainIdentifier(chainIdentifier) {
3096
+ const removingIds = [];
3097
+ for (const history of this.recentSkipHistoryMap.values()) {
3098
+ if (cosmos_1.ChainIdHelper.parse(history.chainId).identifier === chainIdentifier) {
3099
+ removingIds.push(history.id);
3100
+ continue;
3101
+ }
3102
+ if (cosmos_1.ChainIdHelper.parse(history.destinationChainId).identifier ===
3103
+ chainIdentifier) {
3104
+ removingIds.push(history.id);
3105
+ continue;
3106
+ }
3107
+ if (cosmos_1.ChainIdHelper.parse(history.destinationAsset.chainId).identifier ===
3108
+ chainIdentifier) {
3109
+ removingIds.push(history.id);
3110
+ continue;
3111
+ }
3112
+ if (history.simpleRoute.some((route) => cosmos_1.ChainIdHelper.parse(route.chainId).identifier === chainIdentifier)) {
3113
+ removingIds.push(history.id);
3114
+ continue;
3115
+ }
3116
+ if (history.swapRefundInfo &&
3117
+ cosmos_1.ChainIdHelper.parse(history.swapRefundInfo.chainId).identifier ===
3118
+ chainIdentifier) {
3119
+ removingIds.push(history.id);
3120
+ continue;
3121
+ }
3122
+ if (history.transferAssetRelease) {
3123
+ const chainId = history.transferAssetRelease.chain_id;
3124
+ const isOnlyEvm = parseInt(chainId) > 0;
3125
+ const chainIdInKeplr = isOnlyEvm ? `eip155:${chainId}` : chainId;
3126
+ if (cosmos_1.ChainIdHelper.parse(chainIdInKeplr).identifier === chainIdentifier) {
3127
+ removingIds.push(history.id);
3128
+ continue;
3129
+ }
3130
+ }
3131
+ }
3132
+ for (const id of removingIds) {
3133
+ this.recentSkipHistoryMap.delete(id);
3134
+ }
3135
+ }
3136
+ removeSwapV2HistoriesByChainIdentifier(chainIdentifier) {
3137
+ const removingIds = [];
3138
+ for (const history of this.recentSwapV2HistoryMap.values()) {
3139
+ if (cosmos_1.ChainIdHelper.parse(history.fromChainId).identifier === chainIdentifier) {
3140
+ removingIds.push(history.id);
3141
+ continue;
3142
+ }
3143
+ if (cosmos_1.ChainIdHelper.parse(history.toChainId).identifier === chainIdentifier) {
3144
+ removingIds.push(history.id);
3145
+ continue;
3146
+ }
3147
+ if (history.simpleRoute.some((route) => cosmos_1.ChainIdHelper.parse(route.chainId).identifier === chainIdentifier)) {
3148
+ removingIds.push(history.id);
3149
+ continue;
3150
+ }
3151
+ if (history.assetLocationInfo &&
3152
+ cosmos_1.ChainIdHelper.parse(history.assetLocationInfo.chainId).identifier ===
3153
+ chainIdentifier) {
3154
+ removingIds.push(history.id);
3155
+ continue;
3156
+ }
3157
+ if (history.additionalTrackingData) {
3158
+ if (cosmos_1.ChainIdHelper.parse(history.additionalTrackingData.chainId)
3159
+ .identifier === chainIdentifier) {
3160
+ removingIds.push(history.id);
3161
+ continue;
3162
+ }
3163
+ if (history.additionalTrackingData.type === "cosmos-ibc" &&
3164
+ history.additionalTrackingData.ibcHistory.some((h) => {
3165
+ return (cosmos_1.ChainIdHelper.parse(h.counterpartyChainId).identifier ===
3166
+ chainIdentifier);
3167
+ })) {
3168
+ removingIds.push(history.id);
3169
+ continue;
3170
+ }
3171
+ }
3172
+ }
3173
+ for (const id of removingIds) {
3174
+ this.recentSwapV2HistoryMap.delete(id);
3175
+ }
3176
+ }
3177
+ // ============================================================================
3178
+ // Helper Functions
3179
+ // ============================================================================
3180
+ getExecutableChainIdsFromSwapV2History(history, includeAllChainIds = false) {
3181
+ const chainIds = [];
3182
+ const endIndex = includeAllChainIds
3183
+ ? history.simpleRoute.length
3184
+ : Math.max(0, history.routeIndex + 1);
3185
+ for (let i = 0; i < endIndex; i++) {
3186
+ chainIds.push(history.simpleRoute[i].chainId);
3187
+ }
3188
+ if (history.assetLocationInfo &&
3189
+ history.assetLocationInfo.type === "intermediate") {
3190
+ chainIds.push(history.assetLocationInfo.chainId);
3191
+ }
3192
+ return chainIds;
3193
+ }
1945
3194
  }
1946
3195
  __decorate([
1947
3196
  mobx_1.observable
@@ -1958,6 +3207,12 @@ __decorate([
1958
3207
  __decorate([
1959
3208
  mobx_1.observable
1960
3209
  ], RecentSendHistoryService.prototype, "recentSkipHistoryMap", void 0);
3210
+ __decorate([
3211
+ mobx_1.observable
3212
+ ], RecentSendHistoryService.prototype, "recentSwapV2HistorySeq", void 0);
3213
+ __decorate([
3214
+ mobx_1.observable
3215
+ ], RecentSendHistoryService.prototype, "recentSwapV2HistoryMap", void 0);
1961
3216
  __decorate([
1962
3217
  mobx_1.action
1963
3218
  ], RecentSendHistoryService.prototype, "addRecentSendHistory", null);
@@ -1982,5 +3237,41 @@ __decorate([
1982
3237
  __decorate([
1983
3238
  mobx_1.action
1984
3239
  ], RecentSendHistoryService.prototype, "clearAllRecentSkipHistory", null);
3240
+ __decorate([
3241
+ mobx_1.action
3242
+ ], RecentSendHistoryService.prototype, "recordTxWithSwapV2", null);
3243
+ __decorate([
3244
+ mobx_1.action
3245
+ ], RecentSendHistoryService.prototype, "processSwapV2StatusResponse", null);
3246
+ __decorate([
3247
+ mobx_1.action
3248
+ ], RecentSendHistoryService.prototype, "removeRecentSwapV2History", null);
3249
+ __decorate([
3250
+ mobx_1.action
3251
+ ], RecentSendHistoryService.prototype, "clearAllRecentSwapV2History", null);
3252
+ __decorate([
3253
+ mobx_1.action
3254
+ ], RecentSendHistoryService.prototype, "hideSwapV2History", null);
3255
+ __decorate([
3256
+ mobx_1.action
3257
+ ], RecentSendHistoryService.prototype, "showSwapV2History", null);
3258
+ __decorate([
3259
+ mobx_1.action
3260
+ ], RecentSendHistoryService.prototype, "setSwapV2HistoryError", null);
3261
+ __decorate([
3262
+ mobx_1.action
3263
+ ], RecentSendHistoryService.prototype, "clearSwapV2HistoryBackgroundExecutionId", null);
3264
+ __decorate([
3265
+ mobx_1.action
3266
+ ], RecentSendHistoryService.prototype, "setSwapV2AdditionalTrackingData", null);
3267
+ __decorate([
3268
+ mobx_1.action
3269
+ ], RecentSendHistoryService.prototype, "removeIBCHistoriesByChainIdentifier", null);
3270
+ __decorate([
3271
+ mobx_1.action
3272
+ ], RecentSendHistoryService.prototype, "removeSkipHistoriesByChainIdentifier", null);
3273
+ __decorate([
3274
+ mobx_1.action
3275
+ ], RecentSendHistoryService.prototype, "removeSwapV2HistoriesByChainIdentifier", null);
1985
3276
  exports.RecentSendHistoryService = RecentSendHistoryService;
1986
3277
  //# sourceMappingURL=service.js.map