@ledgerhq/coin-hedera 1.15.0-nightly.20251126023856 → 1.15.0-nightly.20251126160702

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 (280) hide show
  1. package/CHANGELOG.md +10 -8
  2. package/lib/api/index.d.ts.map +1 -1
  3. package/lib/api/index.js +6 -2
  4. package/lib/api/index.js.map +1 -1
  5. package/lib/bridge/broadcast.js +1 -1
  6. package/lib/bridge/broadcast.js.map +1 -1
  7. package/lib/bridge/buildOptimisticOperation.d.ts.map +1 -1
  8. package/lib/bridge/buildOptimisticOperation.js +80 -15
  9. package/lib/bridge/buildOptimisticOperation.js.map +1 -1
  10. package/lib/bridge/estimateMaxSpendable.js +5 -2
  11. package/lib/bridge/estimateMaxSpendable.js.map +1 -1
  12. package/lib/bridge/getTransactionStatus.d.ts.map +1 -1
  13. package/lib/bridge/getTransactionStatus.js +70 -13
  14. package/lib/bridge/getTransactionStatus.js.map +1 -1
  15. package/lib/bridge/index.js +1 -1
  16. package/lib/bridge/index.js.map +1 -1
  17. package/lib/bridge/prepareTransaction.d.ts.map +1 -1
  18. package/lib/bridge/prepareTransaction.js +40 -7
  19. package/lib/bridge/prepareTransaction.js.map +1 -1
  20. package/lib/bridge/signOperation.d.ts.map +1 -1
  21. package/lib/bridge/signOperation.js +19 -2
  22. package/lib/bridge/signOperation.js.map +1 -1
  23. package/lib/bridge/synchronisation.d.ts +2 -0
  24. package/lib/bridge/synchronisation.d.ts.map +1 -1
  25. package/lib/bridge/synchronisation.js +101 -30
  26. package/lib/bridge/synchronisation.js.map +1 -1
  27. package/lib/bridge/utils.d.ts +35 -2
  28. package/lib/bridge/utils.d.ts.map +1 -1
  29. package/lib/bridge/utils.js +215 -16
  30. package/lib/bridge/utils.js.map +1 -1
  31. package/lib/constants.d.ts +22 -2
  32. package/lib/constants.d.ts.map +1 -1
  33. package/lib/constants.js +42 -2
  34. package/lib/constants.js.map +1 -1
  35. package/lib/deviceTransactionConfig.d.ts +1 -1
  36. package/lib/deviceTransactionConfig.d.ts.map +1 -1
  37. package/lib/deviceTransactionConfig.js +8 -0
  38. package/lib/deviceTransactionConfig.js.map +1 -1
  39. package/lib/errors.d.ts +3 -0
  40. package/lib/errors.d.ts.map +1 -1
  41. package/lib/errors.js +2 -1
  42. package/lib/errors.js.map +1 -1
  43. package/lib/logic/craftTransaction.d.ts +4 -4
  44. package/lib/logic/craftTransaction.d.ts.map +1 -1
  45. package/lib/logic/craftTransaction.js +46 -5
  46. package/lib/logic/craftTransaction.js.map +1 -1
  47. package/lib/logic/estimateFees.d.ts +2 -4
  48. package/lib/logic/estimateFees.d.ts.map +1 -1
  49. package/lib/logic/estimateFees.js +45 -5
  50. package/lib/logic/estimateFees.js.map +1 -1
  51. package/lib/logic/listOperations.d.ts.map +1 -1
  52. package/lib/logic/listOperations.js +7 -3
  53. package/lib/logic/listOperations.js.map +1 -1
  54. package/lib/logic/utils.d.ts +24 -2
  55. package/lib/logic/utils.d.ts.map +1 -1
  56. package/lib/logic/utils.js +66 -13
  57. package/lib/logic/utils.js.map +1 -1
  58. package/lib/network/api.d.ts +12 -1
  59. package/lib/network/api.d.ts.map +1 -1
  60. package/lib/network/api.js +91 -19
  61. package/lib/network/api.js.map +1 -1
  62. package/lib/network/rpc.d.ts.map +1 -1
  63. package/lib/network/rpc.js +1 -0
  64. package/lib/network/rpc.js.map +1 -1
  65. package/lib/network/thirdweb.d.ts +21 -0
  66. package/lib/network/thirdweb.d.ts.map +1 -0
  67. package/lib/network/thirdweb.js +72 -0
  68. package/lib/network/thirdweb.js.map +1 -0
  69. package/lib/network/utils.d.ts +4 -1
  70. package/lib/network/utils.d.ts.map +1 -1
  71. package/lib/network/utils.js +53 -1
  72. package/lib/network/utils.js.map +1 -1
  73. package/lib/test/bridgeDatasetTest.d.ts.map +1 -1
  74. package/lib/test/bridgeDatasetTest.js +4 -4
  75. package/lib/test/bridgeDatasetTest.js.map +1 -1
  76. package/lib/test/fixtures/account.fixture.js +1 -1
  77. package/lib/test/fixtures/account.fixture.js.map +1 -1
  78. package/lib/test/fixtures/common.fixture.d.ts +12 -0
  79. package/lib/test/fixtures/common.fixture.d.ts.map +1 -0
  80. package/lib/test/fixtures/common.fixture.js +66 -0
  81. package/lib/test/fixtures/common.fixture.js.map +1 -0
  82. package/lib/test/fixtures/currency.fixture.d.ts +3 -1
  83. package/lib/test/fixtures/currency.fixture.d.ts.map +1 -1
  84. package/lib/test/fixtures/currency.fixture.js +63 -16
  85. package/lib/test/fixtures/currency.fixture.js.map +1 -1
  86. package/lib/test/fixtures/mirror.fixture.d.ts +3 -1
  87. package/lib/test/fixtures/mirror.fixture.d.ts.map +1 -1
  88. package/lib/test/fixtures/mirror.fixture.js +12 -1
  89. package/lib/test/fixtures/mirror.fixture.js.map +1 -1
  90. package/lib/test/fixtures/thirdweb.fixture.d.ts +3 -0
  91. package/lib/test/fixtures/thirdweb.fixture.d.ts.map +1 -0
  92. package/lib/test/fixtures/thirdweb.fixture.js +34 -0
  93. package/lib/test/fixtures/thirdweb.fixture.js.map +1 -0
  94. package/lib/transaction.d.ts.map +1 -1
  95. package/lib/transaction.js +2 -0
  96. package/lib/transaction.js.map +1 -1
  97. package/lib/types/alpaca.d.ts +5 -1
  98. package/lib/types/alpaca.d.ts.map +1 -1
  99. package/lib/types/bridge.d.ts +6 -1
  100. package/lib/types/bridge.d.ts.map +1 -1
  101. package/lib/types/index.d.ts +2 -0
  102. package/lib/types/index.d.ts.map +1 -1
  103. package/lib/types/index.js +2 -0
  104. package/lib/types/index.js.map +1 -1
  105. package/lib/types/logic.d.ts +39 -0
  106. package/lib/types/logic.d.ts.map +1 -0
  107. package/lib/types/logic.js +3 -0
  108. package/lib/types/logic.js.map +1 -0
  109. package/lib/types/mirror.d.ts +29 -0
  110. package/lib/types/mirror.d.ts.map +1 -1
  111. package/lib/types/thirdweb.d.ts +34 -0
  112. package/lib/types/thirdweb.d.ts.map +1 -0
  113. package/lib/types/thirdweb.js +3 -0
  114. package/lib/types/thirdweb.js.map +1 -0
  115. package/lib-es/api/index.d.ts.map +1 -1
  116. package/lib-es/api/index.js +7 -3
  117. package/lib-es/api/index.js.map +1 -1
  118. package/lib-es/bridge/broadcast.js +2 -2
  119. package/lib-es/bridge/broadcast.js.map +1 -1
  120. package/lib-es/bridge/buildOptimisticOperation.d.ts.map +1 -1
  121. package/lib-es/bridge/buildOptimisticOperation.js +83 -18
  122. package/lib-es/bridge/buildOptimisticOperation.js.map +1 -1
  123. package/lib-es/bridge/estimateMaxSpendable.js +5 -2
  124. package/lib-es/bridge/estimateMaxSpendable.js.map +1 -1
  125. package/lib-es/bridge/getTransactionStatus.d.ts.map +1 -1
  126. package/lib-es/bridge/getTransactionStatus.js +73 -16
  127. package/lib-es/bridge/getTransactionStatus.js.map +1 -1
  128. package/lib-es/bridge/index.js +2 -2
  129. package/lib-es/bridge/index.js.map +1 -1
  130. package/lib-es/bridge/prepareTransaction.d.ts.map +1 -1
  131. package/lib-es/bridge/prepareTransaction.js +42 -9
  132. package/lib-es/bridge/prepareTransaction.js.map +1 -1
  133. package/lib-es/bridge/signOperation.d.ts.map +1 -1
  134. package/lib-es/bridge/signOperation.js +21 -4
  135. package/lib-es/bridge/signOperation.js.map +1 -1
  136. package/lib-es/bridge/synchronisation.d.ts +2 -0
  137. package/lib-es/bridge/synchronisation.d.ts.map +1 -1
  138. package/lib-es/bridge/synchronisation.js +97 -27
  139. package/lib-es/bridge/synchronisation.js.map +1 -1
  140. package/lib-es/bridge/utils.d.ts +35 -2
  141. package/lib-es/bridge/utils.d.ts.map +1 -1
  142. package/lib-es/bridge/utils.js +211 -16
  143. package/lib-es/bridge/utils.js.map +1 -1
  144. package/lib-es/constants.d.ts +22 -2
  145. package/lib-es/constants.d.ts.map +1 -1
  146. package/lib-es/constants.js +38 -1
  147. package/lib-es/constants.js.map +1 -1
  148. package/lib-es/deviceTransactionConfig.d.ts +1 -1
  149. package/lib-es/deviceTransactionConfig.d.ts.map +1 -1
  150. package/lib-es/deviceTransactionConfig.js +8 -0
  151. package/lib-es/deviceTransactionConfig.js.map +1 -1
  152. package/lib-es/errors.d.ts +3 -0
  153. package/lib-es/errors.d.ts.map +1 -1
  154. package/lib-es/errors.js +1 -0
  155. package/lib-es/errors.js.map +1 -1
  156. package/lib-es/logic/craftTransaction.d.ts +4 -4
  157. package/lib-es/logic/craftTransaction.d.ts.map +1 -1
  158. package/lib-es/logic/craftTransaction.js +48 -7
  159. package/lib-es/logic/craftTransaction.js.map +1 -1
  160. package/lib-es/logic/estimateFees.d.ts +2 -4
  161. package/lib-es/logic/estimateFees.d.ts.map +1 -1
  162. package/lib-es/logic/estimateFees.js +47 -7
  163. package/lib-es/logic/estimateFees.js.map +1 -1
  164. package/lib-es/logic/listOperations.d.ts.map +1 -1
  165. package/lib-es/logic/listOperations.js +7 -3
  166. package/lib-es/logic/listOperations.js.map +1 -1
  167. package/lib-es/logic/utils.d.ts +24 -2
  168. package/lib-es/logic/utils.d.ts.map +1 -1
  169. package/lib-es/logic/utils.js +63 -13
  170. package/lib-es/logic/utils.js.map +1 -1
  171. package/lib-es/network/api.d.ts +12 -1
  172. package/lib-es/network/api.d.ts.map +1 -1
  173. package/lib-es/network/api.js +91 -19
  174. package/lib-es/network/api.js.map +1 -1
  175. package/lib-es/network/rpc.d.ts.map +1 -1
  176. package/lib-es/network/rpc.js +1 -0
  177. package/lib-es/network/rpc.js.map +1 -1
  178. package/lib-es/network/thirdweb.d.ts +21 -0
  179. package/lib-es/network/thirdweb.d.ts.map +1 -0
  180. package/lib-es/network/thirdweb.js +66 -0
  181. package/lib-es/network/thirdweb.js.map +1 -0
  182. package/lib-es/network/utils.d.ts +4 -1
  183. package/lib-es/network/utils.d.ts.map +1 -1
  184. package/lib-es/network/utils.js +49 -0
  185. package/lib-es/network/utils.js.map +1 -1
  186. package/lib-es/test/bridgeDatasetTest.d.ts.map +1 -1
  187. package/lib-es/test/bridgeDatasetTest.js +4 -4
  188. package/lib-es/test/bridgeDatasetTest.js.map +1 -1
  189. package/lib-es/test/fixtures/account.fixture.js +2 -2
  190. package/lib-es/test/fixtures/account.fixture.js.map +1 -1
  191. package/lib-es/test/fixtures/common.fixture.d.ts +12 -0
  192. package/lib-es/test/fixtures/common.fixture.d.ts.map +1 -0
  193. package/lib-es/test/fixtures/common.fixture.js +57 -0
  194. package/lib-es/test/fixtures/common.fixture.js.map +1 -0
  195. package/lib-es/test/fixtures/currency.fixture.d.ts +3 -1
  196. package/lib-es/test/fixtures/currency.fixture.d.ts.map +1 -1
  197. package/lib-es/test/fixtures/currency.fixture.js +59 -14
  198. package/lib-es/test/fixtures/currency.fixture.js.map +1 -1
  199. package/lib-es/test/fixtures/mirror.fixture.d.ts +3 -1
  200. package/lib-es/test/fixtures/mirror.fixture.d.ts.map +1 -1
  201. package/lib-es/test/fixtures/mirror.fixture.js +9 -0
  202. package/lib-es/test/fixtures/mirror.fixture.js.map +1 -1
  203. package/lib-es/test/fixtures/thirdweb.fixture.d.ts +3 -0
  204. package/lib-es/test/fixtures/thirdweb.fixture.d.ts.map +1 -0
  205. package/lib-es/test/fixtures/thirdweb.fixture.js +30 -0
  206. package/lib-es/test/fixtures/thirdweb.fixture.js.map +1 -0
  207. package/lib-es/transaction.d.ts.map +1 -1
  208. package/lib-es/transaction.js +2 -0
  209. package/lib-es/transaction.js.map +1 -1
  210. package/lib-es/types/alpaca.d.ts +5 -1
  211. package/lib-es/types/alpaca.d.ts.map +1 -1
  212. package/lib-es/types/bridge.d.ts +6 -1
  213. package/lib-es/types/bridge.d.ts.map +1 -1
  214. package/lib-es/types/index.d.ts +2 -0
  215. package/lib-es/types/index.d.ts.map +1 -1
  216. package/lib-es/types/index.js +2 -0
  217. package/lib-es/types/index.js.map +1 -1
  218. package/lib-es/types/logic.d.ts +39 -0
  219. package/lib-es/types/logic.d.ts.map +1 -0
  220. package/lib-es/types/logic.js +2 -0
  221. package/lib-es/types/logic.js.map +1 -0
  222. package/lib-es/types/mirror.d.ts +29 -0
  223. package/lib-es/types/mirror.d.ts.map +1 -1
  224. package/lib-es/types/thirdweb.d.ts +34 -0
  225. package/lib-es/types/thirdweb.d.ts.map +1 -0
  226. package/lib-es/types/thirdweb.js +2 -0
  227. package/lib-es/types/thirdweb.js.map +1 -0
  228. package/package.json +9 -8
  229. package/src/api/index.integ.test.ts +11 -8
  230. package/src/api/index.ts +10 -3
  231. package/src/bridge/broadcast.ts +2 -2
  232. package/src/bridge/buildOptimisticOperation.integration.test.ts +70 -19
  233. package/src/bridge/buildOptimisticOperation.ts +98 -20
  234. package/src/bridge/estimateMaxSpendable.ts +5 -5
  235. package/src/bridge/getTransactionStatus.test.ts +57 -12
  236. package/src/bridge/getTransactionStatus.ts +88 -15
  237. package/src/bridge/index.ts +2 -2
  238. package/src/bridge/js-estimateMaxSpendable.integration.test.ts +12 -9
  239. package/src/bridge/prepareTransaction.test.ts +3 -1
  240. package/src/bridge/prepareTransaction.ts +45 -10
  241. package/src/bridge/signOperation.ts +23 -5
  242. package/src/bridge/synchronisation.test.ts +67 -0
  243. package/src/bridge/synchronisation.ts +114 -34
  244. package/src/bridge/utils.integration.test.ts +486 -180
  245. package/src/bridge/utils.test.ts +404 -0
  246. package/src/bridge/utils.ts +330 -27
  247. package/src/constants.ts +47 -2
  248. package/src/deviceTransactionConfig.ts +10 -1
  249. package/src/errors.ts +3 -0
  250. package/src/logic/craftTransaction.test.ts +49 -9
  251. package/src/logic/craftTransaction.ts +76 -11
  252. package/src/logic/estimateFees.test.ts +180 -31
  253. package/src/logic/estimateFees.ts +68 -7
  254. package/src/logic/getAssetFromToken.test.ts +2 -2
  255. package/src/logic/getBalance.test.ts +18 -57
  256. package/src/logic/getTokenFromAsset.test.ts +2 -2
  257. package/src/logic/listOperations.ts +9 -5
  258. package/src/logic/utils.test.ts +157 -69
  259. package/src/logic/utils.ts +75 -13
  260. package/src/network/api.test.ts +211 -3
  261. package/src/network/api.ts +118 -24
  262. package/src/network/rpc.test.ts +1 -0
  263. package/src/network/rpc.ts +1 -0
  264. package/src/network/thirdweb.test.ts +188 -0
  265. package/src/network/thirdweb.ts +101 -0
  266. package/src/network/utils.test.ts +364 -164
  267. package/src/network/utils.ts +83 -1
  268. package/src/test/bridgeDatasetTest.ts +4 -5
  269. package/src/test/fixtures/account.fixture.ts +2 -2
  270. package/src/test/fixtures/common.fixture.ts +74 -0
  271. package/src/test/fixtures/currency.fixture.ts +66 -14
  272. package/src/test/fixtures/mirror.fixture.ts +23 -1
  273. package/src/test/fixtures/thirdweb.fixture.ts +33 -0
  274. package/src/transaction.ts +2 -0
  275. package/src/types/alpaca.ts +8 -1
  276. package/src/types/bridge.ts +6 -1
  277. package/src/types/index.ts +2 -0
  278. package/src/types/logic.ts +44 -0
  279. package/src/types/mirror.ts +35 -0
  280. package/src/types/thirdweb.ts +36 -0
@@ -4,20 +4,20 @@ import { setupCalClientStore } from "@ledgerhq/cryptoassets/cal-client/test-help
4
4
  import { getCryptoAssetsStore } from "@ledgerhq/cryptoassets/state";
5
5
  import { HEDERA_OPERATION_TYPES, HEDERA_TRANSACTION_MODES } from "../constants";
6
6
  import { estimateFees } from "../logic/estimateFees";
7
+ import { toEVMAddress } from "../logic/utils";
8
+ import { apiClient } from "../network/api";
7
9
  import { getMockedAccount, getMockedTokenAccount } from "../test/fixtures/account.fixture";
8
- import { getMockedTokenCurrency, getTokenCurrencyFromCAL } from "../test/fixtures/currency.fixture";
9
- import { getMockedTransaction } from "../test/fixtures/transaction.fixture";
10
- import { getMockedOperation } from "../test/fixtures/operation.fixture";
11
- import { getMockedMirrorToken } from "../test/fixtures/mirror.fixture";
12
- import type { HederaOperationExtra } from "../types";
13
10
  import {
14
- applyPendingExtras,
15
- calculateAmount,
16
- getSubAccounts,
17
- mergeSubAccounts,
18
- patchOperationWithExtra,
19
- prepareOperations,
20
- } from "./utils";
11
+ getMockedHTSTokenCurrency,
12
+ getTokenCurrencyFromCAL,
13
+ getTokenCurrencyFromCALByType,
14
+ } from "../test/fixtures/currency.fixture";
15
+ import { getMockedMirrorToken } from "../test/fixtures/mirror.fixture";
16
+ import { getMockedOperation } from "../test/fixtures/operation.fixture";
17
+ import { getMockedThirdwebTransaction } from "../test/fixtures/thirdweb.fixture";
18
+ import { getMockedTransaction } from "../test/fixtures/transaction.fixture";
19
+ import type { EstimateFeesResult } from "../types";
20
+ import { calculateAmount, getSubAccounts, integrateERC20Operations } from "./utils";
21
21
 
22
22
  describe("utils", () => {
23
23
  beforeAll(() => {
@@ -26,24 +26,30 @@ describe("utils", () => {
26
26
  });
27
27
 
28
28
  describe("calculateAmount", () => {
29
- let estimatedFees: Record<"crypto" | "associate", BigNumber>;
29
+ let estimatedFees: Record<"crypto" | "associate", EstimateFeesResult>;
30
30
 
31
31
  beforeAll(async () => {
32
32
  const mockedAccount = getMockedAccount();
33
33
  const [crypto, associate] = await Promise.all([
34
- estimateFees(mockedAccount.currency, HEDERA_OPERATION_TYPES.CryptoTransfer),
35
- estimateFees(mockedAccount.currency, HEDERA_OPERATION_TYPES.TokenAssociate),
34
+ estimateFees({
35
+ currency: mockedAccount.currency,
36
+ operationType: HEDERA_OPERATION_TYPES.CryptoTransfer,
37
+ }),
38
+ estimateFees({
39
+ currency: mockedAccount.currency,
40
+ operationType: HEDERA_OPERATION_TYPES.TokenAssociate,
41
+ }),
36
42
  ]);
37
43
 
38
44
  estimatedFees = { crypto, associate };
39
45
  });
40
46
 
41
- test("HBAR transfer, useAllAmount = true", async () => {
47
+ it("HBAR transfer, useAllAmount = true", async () => {
42
48
  const mockedAccount = getMockedAccount();
43
49
  const mockedTransaction = getMockedTransaction({ useAllAmount: true });
44
50
 
45
- const amount = mockedAccount.balance.minus(estimatedFees.crypto);
46
- const totalSpent = amount.plus(estimatedFees.crypto);
51
+ const amount = mockedAccount.balance.minus(estimatedFees.crypto.tinybars);
52
+ const totalSpent = amount.plus(estimatedFees.crypto.tinybars);
47
53
 
48
54
  const result = await calculateAmount({
49
55
  account: mockedAccount,
@@ -53,7 +59,7 @@ describe("utils", () => {
53
59
  expect(result).toEqual({ amount, totalSpent });
54
60
  });
55
61
 
56
- test("HBAR transfer, useAllAmount = false", async () => {
62
+ it("HBAR transfer, useAllAmount = false", async () => {
57
63
  const mockedAccount = getMockedAccount();
58
64
  const mockedTransaction = getMockedTransaction({
59
65
  useAllAmount: false,
@@ -61,7 +67,7 @@ describe("utils", () => {
61
67
  });
62
68
 
63
69
  const amount = mockedTransaction.amount;
64
- const totalSpent = amount.plus(estimatedFees.crypto);
70
+ const totalSpent = amount.plus(estimatedFees.crypto.tinybars);
65
71
 
66
72
  const result = await calculateAmount({
67
73
  account: mockedAccount,
@@ -71,8 +77,8 @@ describe("utils", () => {
71
77
  expect(result).toEqual({ amount, totalSpent });
72
78
  });
73
79
 
74
- test("token transfer, useAllAmount = true", async () => {
75
- const mockedTokenCurrency = getMockedTokenCurrency();
80
+ it("token transfer, useAllAmount = true", async () => {
81
+ const mockedTokenCurrency = getMockedHTSTokenCurrency();
76
82
  const mockedTokenAccount = getMockedTokenAccount(mockedTokenCurrency);
77
83
  const mockedAccount = getMockedAccount({ subAccounts: [mockedTokenAccount] });
78
84
  const mockedTransaction = getMockedTransaction({
@@ -91,8 +97,8 @@ describe("utils", () => {
91
97
  expect(result).toEqual({ amount, totalSpent });
92
98
  });
93
99
 
94
- test("token transfer, useAllAmount = false", async () => {
95
- const mockedTokenCurrency = getMockedTokenCurrency();
100
+ it("token transfer, useAllAmount = false", async () => {
101
+ const mockedTokenCurrency = getMockedHTSTokenCurrency();
96
102
  const mockedTokenAccount = getMockedTokenAccount(mockedTokenCurrency);
97
103
  const mockedAccount = getMockedAccount({ subAccounts: [mockedTokenAccount] });
98
104
  const mockedTransaction = getMockedTransaction({
@@ -112,8 +118,8 @@ describe("utils", () => {
112
118
  expect(result).toEqual({ amount, totalSpent });
113
119
  });
114
120
 
115
- test("token associate operation uses TokenAssociate fee", async () => {
116
- const mockedTokenCurrency = getMockedTokenCurrency();
121
+ it("token associate operation uses TokenAssociate fee", async () => {
122
+ const mockedTokenCurrency = getMockedHTSTokenCurrency();
117
123
  const mockedTokenAccount = getMockedTokenAccount(mockedTokenCurrency);
118
124
  const mockedAccount = getMockedAccount({ subAccounts: [mockedTokenAccount] });
119
125
  const mockedTransaction = getMockedTransaction({
@@ -126,7 +132,7 @@ describe("utils", () => {
126
132
  });
127
133
 
128
134
  const amount = mockedTransaction.amount;
129
- const totalSpent = amount.plus(estimatedFees.associate);
135
+ const totalSpent = amount.plus(estimatedFees.associate.tinybars);
130
136
 
131
137
  const result = await calculateAmount({
132
138
  account: mockedAccount,
@@ -138,7 +144,7 @@ describe("utils", () => {
138
144
  });
139
145
 
140
146
  describe("getSubAccounts", () => {
141
- test("returns sub account based on operations and mirror tokens", async () => {
147
+ it("returns sub account based on operations and mirror tokens", async () => {
142
148
  const firstTokenCurrencyFromCAL = getTokenCurrencyFromCAL(0);
143
149
  const secondTokenCurrencyFromCAL = getTokenCurrencyFromCAL(1);
144
150
  const mockedAccount = getMockedAccount();
@@ -172,215 +178,515 @@ describe("utils", () => {
172
178
  accountId: encodeTokenAccountId(mockedAccount.id, secondTokenFromCAL),
173
179
  });
174
180
 
175
- const result = await getSubAccounts(
176
- mockedAccount.id,
177
- [mockedOperation1, mockedOperation2],
178
- [mockedMirrorToken1, mockedMirrorToken2],
179
- );
181
+ const result = await getSubAccounts({
182
+ ledgerAccountId: mockedAccount.id,
183
+ latestHTSTokenOperations: [mockedOperation1, mockedOperation2],
184
+ latestERC20TokenOperations: [],
185
+ mirrorTokens: [mockedMirrorToken1, mockedMirrorToken2],
186
+ erc20Tokens: [],
187
+ });
180
188
  const uniqueSubAccountIds = new Set(result.map(sa => sa.id));
181
189
 
182
- expect(result).toHaveLength(2);
183
- // Compare contract address instead of full object since CAL format may differ
184
- expect(result[0].token.contractAddress).toBe(firstTokenCurrencyFromCAL.contractAddress);
185
- expect(result[1].token.contractAddress).toBe(secondTokenCurrencyFromCAL.contractAddress);
186
- expect(result[0].balance).toEqual(new BigNumber(10));
187
- expect(result[1].balance).toEqual(new BigNumber(0));
188
- expect(result[0].operations).toEqual([mockedOperation1]);
189
- expect(result[1].operations).toEqual([mockedOperation2]);
190
190
  expect(uniqueSubAccountIds.size).toBe(result.length);
191
+ expect(result).toHaveLength(2);
192
+ expect(result).toMatchObject([
193
+ {
194
+ token: firstTokenCurrencyFromCAL,
195
+ balance: new BigNumber(10),
196
+ operations: [mockedOperation1],
197
+ },
198
+ {
199
+ token: secondTokenCurrencyFromCAL,
200
+ balance: new BigNumber(0),
201
+ operations: [mockedOperation2],
202
+ },
203
+ ]);
191
204
  });
192
205
 
193
- test("ignores operation if token is not listed in CAL", async () => {
194
- const mockedTokenCurrency = getMockedTokenCurrency();
206
+ it("ignores operation if token is not listed in CAL", async () => {
207
+ const mockedTokenCurrency = getMockedHTSTokenCurrency();
195
208
  const mockedAccount = getMockedAccount();
196
209
  const mockedOperation = getMockedOperation({
197
210
  accountId: encodeTokenAccountId(mockedAccount.id, mockedTokenCurrency),
198
211
  });
199
212
 
200
- const result = await getSubAccounts(mockedAccount.id, [mockedOperation], []);
213
+ const result = await getSubAccounts({
214
+ ledgerAccountId: mockedAccount.id,
215
+ latestHTSTokenOperations: [mockedOperation],
216
+ latestERC20TokenOperations: [],
217
+ mirrorTokens: [],
218
+ erc20Tokens: [],
219
+ });
201
220
 
202
221
  expect(result).toHaveLength(0);
203
222
  });
204
223
 
205
- test("returns sub account for mirror token with no operations yet (e.g. right after association)", async () => {
224
+ it("returns sub account for mirror token with no operations yet (e.g. right after association)", async () => {
206
225
  const tokenCurrencyFromCAL = getTokenCurrencyFromCAL(0);
207
226
  const mockedAccount = getMockedAccount();
208
- const mockedMirrorToken = getMockedMirrorToken({
227
+ const mockedTokenHTS = getMockedMirrorToken({
209
228
  token_id: tokenCurrencyFromCAL.contractAddress,
210
229
  balance: 42,
211
230
  });
212
231
 
213
- const result = await getSubAccounts(mockedAccount.id, [], [mockedMirrorToken]);
232
+ const result = await getSubAccounts({
233
+ ledgerAccountId: mockedAccount.id,
234
+ latestHTSTokenOperations: [],
235
+ latestERC20TokenOperations: [],
236
+ mirrorTokens: [mockedTokenHTS],
237
+ erc20Tokens: [],
238
+ });
214
239
 
215
240
  expect(result).toHaveLength(1);
216
- // Compare contract address instead of full object since CAL format may differ
217
- expect(result[0].token.contractAddress).toBe(tokenCurrencyFromCAL.contractAddress);
218
- expect(result[0].operations).toHaveLength(0);
219
- expect(result[0].balance).toEqual(new BigNumber(42));
241
+ expect(result).toMatchObject([
242
+ {
243
+ token: tokenCurrencyFromCAL,
244
+ operations: [],
245
+ balance: new BigNumber(42),
246
+ },
247
+ ]);
220
248
  });
221
- });
222
-
223
- describe("prepareOperations", () => {
224
- test("links token operation to existing coin operation with matching hash", async () => {
225
- const tokenCurrencyFromCAL = getTokenCurrencyFromCAL(0);
226
- // Fetch actual token from CAL to ensure we use the correct format
227
- const tokenFromCAL = await getCryptoAssetsStore().findTokenByAddressInCurrency(
228
- tokenCurrencyFromCAL.contractAddress,
229
- "hedera",
230
- );
231
249
 
232
- if (!tokenFromCAL) {
233
- throw new Error("Token not found in CAL");
234
- }
250
+ it("returns sub account for erc20 token with no operations yet", async () => {
251
+ const tokenCurrencyFromCAL = getTokenCurrencyFromCALByType("erc20");
252
+ const mockedAccount = getMockedAccount();
235
253
 
236
- const mockedTokenAccount = getMockedTokenAccount(tokenFromCAL);
237
- const mockedCoinOperation = getMockedOperation({ hash: "shared" });
238
- const mockedTokenOperation = getMockedOperation({
239
- hash: "shared",
240
- accountId: encodeTokenAccountId(mockedTokenAccount.parentId, tokenFromCAL),
254
+ const result = await getSubAccounts({
255
+ ledgerAccountId: mockedAccount.id,
256
+ latestHTSTokenOperations: [],
257
+ latestERC20TokenOperations: [],
258
+ mirrorTokens: [],
259
+ erc20Tokens: [{ balance: new BigNumber(42), token: tokenCurrencyFromCAL }],
241
260
  });
242
261
 
243
- const result = await prepareOperations([mockedCoinOperation], [mockedTokenOperation]);
244
-
245
262
  expect(result).toHaveLength(1);
246
- expect(result[0].subOperations).toEqual([mockedTokenOperation]);
263
+ expect(result).toMatchObject([
264
+ {
265
+ token: tokenCurrencyFromCAL,
266
+ operations: [],
267
+ balance: new BigNumber(42),
268
+ },
269
+ ]);
247
270
  });
271
+ });
248
272
 
249
- test("creates NONE coin operation as parent if no coin op with matching hash exists", async () => {
250
- const tokenCurrencyFromCAL = getTokenCurrencyFromCAL(0);
251
- // Fetch actual token from CAL to ensure we use the correct format
252
- const tokenFromCAL = await getCryptoAssetsStore().findTokenByAddressInCurrency(
253
- tokenCurrencyFromCAL.contractAddress,
254
- "hedera",
255
- );
273
+ describe("integrateERC20Operations", () => {
274
+ const address = "0.0.12345";
275
+ const evmAddress = toEVMAddress(address);
276
+ const ledgerAccountId = `js:2:hedera:${address}:`;
277
+ const tokenCurrency = getTokenCurrencyFromCALByType("erc20");
256
278
 
257
- if (!tokenFromCAL) {
258
- throw new Error("Token not found in CAL");
259
- }
279
+ afterEach(() => {
280
+ jest.restoreAllMocks();
281
+ });
260
282
 
261
- const mockedTokenAccount = getMockedTokenAccount(tokenFromCAL);
262
- const mockedOrphanTokenOperation = getMockedOperation({
263
- hash: "unknown-hash",
264
- accountId: encodeTokenAccountId(mockedTokenAccount.parentId, tokenFromCAL),
283
+ it("creates new operation for erc20 in transfer", async () => {
284
+ const mockGetContractCallResult = jest.spyOn(apiClient, "getContractCallResult");
285
+ const mockFindTransactionByContractCall = jest.spyOn(
286
+ apiClient,
287
+ "findTransactionByContractCall",
288
+ );
289
+
290
+ const incomingTxConsensusTimestamp = `1705836000.000000000`;
291
+ const incomingTxHash = "incoming_erc20";
292
+ const incomingTxValue = "3000000";
293
+ const incomingTxFrom = "0xSENDER";
294
+ const incomingTxTo = evmAddress;
295
+ const incomingERC20Transaction = getMockedThirdwebTransaction({
296
+ transactionHash: incomingTxHash,
297
+ address: tokenCurrency.contractAddress,
298
+ blockHash: "0xINCOMING_BLOCK",
299
+ blockNumber: 12345,
300
+ decoded: {
301
+ name: "Transfer",
302
+ signature: "Transfer(address,address,uint256)",
303
+ params: {
304
+ from: incomingTxFrom,
305
+ to: incomingTxTo,
306
+ value: incomingTxValue,
307
+ },
308
+ },
309
+ });
310
+ const oldMirrorOperations = [
311
+ getMockedOperation({
312
+ hash: "normal_tx",
313
+ type: "IN",
314
+ date: new Date("2024-01-20T10:00:00Z"),
315
+ }),
316
+ ];
317
+
318
+ mockGetContractCallResult.mockResolvedValue({
319
+ timestamp: incomingTxConsensusTimestamp,
320
+ contract_id: tokenCurrency.contractAddress,
321
+ } as any);
322
+
323
+ mockFindTransactionByContractCall.mockResolvedValue({
324
+ transaction_hash: incomingTxHash,
325
+ consensus_timestamp: incomingTxConsensusTimestamp,
326
+ } as any);
327
+
328
+ const { updatedOperations, newERC20TokenOperations } = await integrateERC20Operations({
329
+ ledgerAccountId,
330
+ address,
331
+ allOperations: oldMirrorOperations,
332
+ latestERC20Transactions: [incomingERC20Transaction],
333
+ pendingOperationHashes: new Set(),
334
+ erc20OperationHashes: new Set(),
265
335
  });
266
336
 
267
- const result = await prepareOperations([], [mockedOrphanTokenOperation]);
268
- const noneOp = result.find(op => op.type === "NONE");
337
+ const incomingOp = updatedOperations.find(op => op.hash === incomingTxHash);
269
338
 
270
- expect(typeof noneOp).toBe("object");
271
- expect(noneOp).not.toBeNull();
272
- expect(noneOp?.subOperations?.[0]).toEqual(mockedOrphanTokenOperation);
273
- expect(noneOp?.hash).toBe("unknown-hash");
339
+ expect(incomingOp).toMatchObject({
340
+ type: "NONE",
341
+ hash: incomingTxHash,
342
+ blockHash: incomingERC20Transaction.blockHash,
343
+ });
344
+ expect(incomingOp?.subOperations).toHaveLength(1);
345
+ expect(incomingOp?.subOperations).toMatchObject([
346
+ {
347
+ type: "IN",
348
+ hash: incomingTxHash,
349
+ blockHash: incomingERC20Transaction.blockHash,
350
+ standard: "erc20",
351
+ value: new BigNumber(incomingTxValue),
352
+ senders: [incomingTxFrom],
353
+ recipients: [address],
354
+ },
355
+ ]);
356
+ expect(newERC20TokenOperations).toHaveLength(1);
357
+ expect(newERC20TokenOperations).toMatchObject([incomingOp?.subOperations?.[0]]);
358
+ expect(updatedOperations).toHaveLength(oldMirrorOperations.length + 1);
274
359
  });
275
- });
276
360
 
277
- describe("mergeSubAccounts", () => {
278
- test("returns newSubAccounts if no initial account exists", () => {
279
- const mockedTokenCurrency1 = getMockedTokenCurrency({ id: "token1" });
280
- const mockedTokenCurrency2 = getMockedTokenCurrency({ id: "token2" });
281
- const mockedTokenAccount1 = getMockedTokenAccount(mockedTokenCurrency1, { id: "ta1" });
282
- const mockedTokenAccount2 = getMockedTokenAccount(mockedTokenCurrency2, { id: "ta2" });
283
- const initialAccount = undefined;
284
- const newSubAccounts = [mockedTokenAccount1, mockedTokenAccount2];
361
+ it("creates new operation for erc20 out transfer (not made by user)", async () => {
362
+ const mockGetContractCallResult = jest.spyOn(apiClient, "getContractCallResult");
363
+ const mockFindTransactionByContractCall = jest.spyOn(
364
+ apiClient,
365
+ "findTransactionByContractCall",
366
+ );
285
367
 
286
- const result = mergeSubAccounts(initialAccount, newSubAccounts);
368
+ const allowanceTxConsensusTimestamp = "1705922400.000000000";
369
+ const allowanceTxHash = "transfer_by_allowance";
370
+ const allowanceTxValue = "2000000";
371
+ const allowanceTxFrom = evmAddress;
372
+ const allowanceTxTo = "0xRECIPIENT";
373
+
374
+ const oldMirrorOperations = [
375
+ getMockedOperation({
376
+ hash: "normal_tx",
377
+ type: "OUT",
378
+ date: new Date("2024-01-20T10:00:00Z"),
379
+ }),
380
+ ];
381
+
382
+ const allowanceERC20Transaction = getMockedThirdwebTransaction({
383
+ transactionHash: allowanceTxHash,
384
+ address: tokenCurrency.contractAddress,
385
+ blockHash: "0xALLOWANCE_BLOCK",
386
+ blockNumber: 12346,
387
+ decoded: {
388
+ name: "Transfer",
389
+ signature: "Transfer(address,address,uint256)",
390
+ params: {
391
+ from: allowanceTxFrom,
392
+ to: allowanceTxTo,
393
+ value: allowanceTxValue,
394
+ },
395
+ },
396
+ });
287
397
 
288
- expect(result).toEqual(newSubAccounts);
289
- });
398
+ mockGetContractCallResult.mockResolvedValue({
399
+ timestamp: allowanceTxConsensusTimestamp,
400
+ contract_id: tokenCurrency.contractAddress,
401
+ } as any);
402
+
403
+ mockFindTransactionByContractCall.mockResolvedValue({
404
+ transaction_hash: allowanceTxHash,
405
+ consensus_timestamp: allowanceTxConsensusTimestamp,
406
+ } as any);
407
+
408
+ const { updatedOperations, newERC20TokenOperations } = await integrateERC20Operations({
409
+ ledgerAccountId,
410
+ address,
411
+ allOperations: oldMirrorOperations,
412
+ latestERC20Transactions: [allowanceERC20Transaction],
413
+ pendingOperationHashes: new Set(),
414
+ erc20OperationHashes: new Set(),
415
+ });
290
416
 
291
- test("merges operations and updates only changed fields", () => {
292
- const mockedTokenCurrency = getMockedTokenCurrency();
293
- const existingOperation = getMockedOperation({ id: "op1" });
294
- const newOperation = getMockedOperation({ id: "op2" });
295
- const newPendingOperation = getMockedOperation({ id: "op3" });
296
- const existingTokenAccount = getMockedTokenAccount(mockedTokenCurrency, {
297
- id: "tokenaccount",
298
- balance: new BigNumber(1000),
299
- creationDate: new Date(),
300
- operations: [existingOperation],
301
- pendingOperations: [],
302
- });
303
- const updatedTokenAccount = getMockedTokenAccount(mockedTokenCurrency, {
304
- id: "tokenaccount",
305
- balance: new BigNumber(2000),
306
- creationDate: new Date(Date.now() - 24 * 60 * 60 * 1000),
307
- operations: [newOperation],
308
- pendingOperations: [newPendingOperation],
309
- });
310
- const mockedAccount = getMockedAccount({ subAccounts: [existingTokenAccount] });
311
-
312
- const result = mergeSubAccounts(mockedAccount, [updatedTokenAccount]);
313
- const merged = result[0];
417
+ const allowanceOp = updatedOperations.find(op => op.hash === allowanceTxHash);
314
418
 
315
- expect(result).toHaveLength(1);
316
- expect(merged.creationDate).toEqual(existingTokenAccount.creationDate);
317
- expect(merged.balance).toEqual(new BigNumber(2000));
318
- expect(merged.pendingOperations.map(op => op.id)).toEqual(["op3"]);
319
- expect(merged.operations.map(op => op.id)).toEqual(["op2", "op1"]);
320
- expect(merged.operationsCount).toEqual(2);
419
+ expect(allowanceOp).toMatchObject({
420
+ type: "FEES",
421
+ hash: allowanceTxHash,
422
+ blockHash: allowanceERC20Transaction.blockHash,
423
+ standard: "erc20",
424
+ });
425
+ expect(allowanceOp?.subOperations).toHaveLength(1);
426
+ expect(allowanceOp?.subOperations).toMatchObject([
427
+ {
428
+ type: "OUT",
429
+ hash: allowanceTxHash,
430
+ blockHash: allowanceERC20Transaction.blockHash,
431
+ standard: "erc20",
432
+ value: new BigNumber(allowanceTxValue),
433
+ senders: [address],
434
+ recipients: [allowanceTxTo],
435
+ },
436
+ ]);
437
+ expect(newERC20TokenOperations).toHaveLength(1);
438
+ expect(newERC20TokenOperations).toMatchObject([allowanceOp?.subOperations?.[0]]);
439
+ expect(updatedOperations).toHaveLength(oldMirrorOperations.length + 1);
321
440
  });
322
441
 
323
- test("adds new sub accounts that are not present in initial account", () => {
324
- const existingToken = getMockedTokenCurrency({ id: "token1" });
325
- const newToken = getMockedTokenCurrency({ id: "token2" });
326
- const existingTokenAccount = getMockedTokenAccount(existingToken, { id: "ta1" });
327
- const newTokenAccount = getMockedTokenAccount(newToken, { id: "ta2" });
328
- const mockedAccount = getMockedAccount({ subAccounts: [existingTokenAccount] });
329
-
330
- const result = mergeSubAccounts(mockedAccount, [existingTokenAccount, newTokenAccount]);
331
-
332
- expect(result.map(sa => sa.id)).toEqual(["ta1", "ta2"]);
333
- });
334
- });
442
+ it("avoids duplicated CONTRACT_CALL operation if confirmed erc20 operation exists", async () => {
443
+ const mockGetContractCallResult = jest.spyOn(apiClient, "getContractCallResult");
444
+ const mockFindTransactionByContractCall = jest.spyOn(
445
+ apiClient,
446
+ "findTransactionByContractCall",
447
+ );
335
448
 
336
- describe("applyPendingExtras", () => {
337
- test("merges valid extras from pending operations", () => {
338
- const opExtra1: HederaOperationExtra = { consensusTimestamp: "1.2.3.4" };
339
- const pendingExtra1: HederaOperationExtra = { associatedTokenId: "0.0.1234" };
449
+ const duplicateTxConsensusTimestamp = "1705836000.000000000";
450
+ const duplicateTxHash = "duplicate_tx";
451
+
452
+ const operationsWithDuplicate = [
453
+ getMockedOperation({
454
+ hash: duplicateTxHash,
455
+ type: "FEES",
456
+ standard: "erc20",
457
+ date: new Date("2024-01-20T10:00:00Z"),
458
+ blockHash: "0xBLOCK",
459
+ subOperations: [
460
+ getMockedOperation({
461
+ type: "OUT",
462
+ standard: "erc20",
463
+ hash: duplicateTxHash,
464
+ accountId: encodeTokenAccountId(ledgerAccountId, tokenCurrency),
465
+ }),
466
+ ],
467
+ }),
468
+ getMockedOperation({
469
+ hash: duplicateTxHash,
470
+ type: "CONTRACT_CALL",
471
+ date: new Date("2024-01-20T10:00:00Z"),
472
+ }),
473
+ getMockedOperation({
474
+ hash: "unique_tx",
475
+ type: "OUT",
476
+ date: new Date("2024-01-19T10:00:00Z"),
477
+ }),
478
+ ];
479
+
480
+ const duplicateERC20Transaction = getMockedThirdwebTransaction({
481
+ transactionHash: duplicateTxHash,
482
+ address: tokenCurrency.contractAddress,
483
+ blockHash: "0xBLOCK",
484
+ decoded: {
485
+ name: "Transfer",
486
+ signature: "Transfer(address,address,uint256)",
487
+ params: {
488
+ from: evmAddress,
489
+ to: "0xRECIPIENT",
490
+ value: "1000000",
491
+ },
492
+ },
493
+ });
340
494
 
341
- const mockedOperation1 = getMockedOperation({ hash: "op1", extra: opExtra1 });
342
- const mockedPendingOperation1 = getMockedOperation({ hash: "op1", extra: pendingExtra1 });
495
+ mockGetContractCallResult.mockResolvedValue({
496
+ timestamp: duplicateTxConsensusTimestamp,
497
+ contract_id: tokenCurrency.contractAddress,
498
+ } as any);
499
+
500
+ mockFindTransactionByContractCall.mockResolvedValue({
501
+ transaction_hash: duplicateTxHash,
502
+ consensus_timestamp: duplicateTxConsensusTimestamp,
503
+ } as any);
504
+
505
+ const { updatedOperations } = await integrateERC20Operations({
506
+ ledgerAccountId,
507
+ address,
508
+ allOperations: operationsWithDuplicate,
509
+ latestERC20Transactions: [duplicateERC20Transaction],
510
+ pendingOperationHashes: new Set(),
511
+ erc20OperationHashes: new Set([duplicateTxHash]),
512
+ });
343
513
 
344
- const result = applyPendingExtras([mockedOperation1], [mockedPendingOperation1]);
514
+ const duplicatedContractCalls = updatedOperations.filter(
515
+ op => op.type === "CONTRACT_CALL" && op.hash === duplicateTxHash,
516
+ );
517
+ const feesOps = updatedOperations.filter(
518
+ op => op.type === "FEES" && op.hash === duplicateTxHash,
519
+ );
345
520
 
346
- expect(result).toHaveLength(1);
347
- expect(result[0].extra).toEqual({
348
- ...mockedOperation1.extra,
349
- ...mockedPendingOperation1.extra,
350
- });
521
+ expect(updatedOperations).toHaveLength(2);
522
+ expect(duplicatedContractCalls).toHaveLength(0);
523
+ expect(feesOps).toHaveLength(1);
524
+ expect(feesOps).toMatchObject([{ blockHash: "0xBLOCK" }]);
351
525
  });
352
526
 
353
- test("returns original operation if no matching pending is found", () => {
354
- const opExtra: HederaOperationExtra = { consensusTimestamp: "1.2.3.4" };
355
- const pendingExtra: HederaOperationExtra = { associatedTokenId: "0.0.1234" };
527
+ it("avoids duplicated CONTRACT_CALL operation if erc20 operation is pending", async () => {
528
+ const pendingTxHash = "pending_erc20";
529
+
530
+ const operationsWithPending = [
531
+ getMockedOperation({
532
+ hash: pendingTxHash,
533
+ type: "CONTRACT_CALL",
534
+ date: new Date("2024-01-20T10:00:00Z"),
535
+ }),
536
+ getMockedOperation({
537
+ hash: "confirmed_tx",
538
+ type: "OUT",
539
+ date: new Date("2024-01-19T10:00:00Z"),
540
+ }),
541
+ ];
542
+
543
+ const { updatedOperations } = await integrateERC20Operations({
544
+ ledgerAccountId,
545
+ address,
546
+ allOperations: operationsWithPending,
547
+ latestERC20Transactions: [],
548
+ pendingOperationHashes: new Set([pendingTxHash]),
549
+ erc20OperationHashes: new Set(),
550
+ });
356
551
 
357
- const mockedOperation = getMockedOperation({ hash: "unknown", extra: opExtra });
358
- const mockedPendingOperation = getMockedOperation({ hash: "op1", extra: pendingExtra });
552
+ const pendingOp = updatedOperations.find(op => op.hash === pendingTxHash);
359
553
 
360
- const result = applyPendingExtras([mockedOperation], [mockedPendingOperation]);
361
- expect(result).toHaveLength(1);
362
- expect(result[0].extra).toEqual(mockedOperation.extra);
554
+ expect(pendingOp).toBeUndefined();
555
+ expect(updatedOperations).toHaveLength(1);
556
+ expect(updatedOperations[0].hash).toBe("confirmed_tx");
363
557
  });
364
- });
365
558
 
366
- describe("patchOperationWithExtra", () => {
367
- test("adds extra to operation and nested sub operations", () => {
368
- const mockedOperation = getMockedOperation({
369
- hash: "parent",
370
- extra: {},
371
- subOperations: [getMockedOperation({ hash: "sub1", extra: {} })],
559
+ /**
560
+ * Timeline:
561
+ * - Tuesday: Normal transactions
562
+ * - Wednesday: ERC20 transfer (Mirror + Thirdweb in sync)
563
+ * - Thursday: Normal transaction
564
+ * - Friday: ERC20 transfer (Thirdweb stuck - no event)
565
+ * - Saturday: Normal transaction
566
+ *
567
+ * SYNC 1 (Friday):
568
+ * - Mirror Node shows CONTRACT_CALL without blockHash
569
+ * - Thirdweb has no event yet (indexer stuck)
570
+ * - Operation remains as CONTRACT_CALL (not enriched)
571
+ *
572
+ * SYNC 2 (Saturday):
573
+ * - Thirdweb catches up and returns Friday's event
574
+ * - CONTRACT_CALL should get patched to FEES with ERC20 sub-operation
575
+ */
576
+ it("handles delayed thirdweb indexer", async () => {
577
+ const mockGetContractCallResult = jest.spyOn(apiClient, "getContractCallResult");
578
+ const mockFindTransactionByContractCall = jest.spyOn(
579
+ apiClient,
580
+ "findTransactionByContractCall",
581
+ );
582
+
583
+ const fridayTxConsensusTimestamp = `1705678200.000000000`;
584
+
585
+ // sync 1 from Friday: thirdweb hasn't indexed yet
586
+ const fridaySyncOperations = [
587
+ getMockedOperation({
588
+ hash: "saturday_tx",
589
+ type: "OUT",
590
+ date: new Date("2024-01-20T10:00:00Z"),
591
+ }),
592
+ getMockedOperation({
593
+ hash: "friday_erc20",
594
+ type: "CONTRACT_CALL",
595
+ date: new Date("2024-01-19T15:30:00Z"),
596
+ }),
597
+ getMockedOperation({
598
+ hash: "thursday_tx",
599
+ type: "OUT",
600
+ date: new Date("2024-01-18T12:00:00Z"),
601
+ }),
602
+ getMockedOperation({
603
+ hash: "wednesday_erc20",
604
+ type: "FEES",
605
+ date: new Date("2024-01-17T09:00:00Z"),
606
+ standard: "erc20",
607
+ blockHash: "0xWEDNESDAY_BLOCK",
608
+ subOperations: [
609
+ getMockedOperation({
610
+ type: "OUT",
611
+ standard: "erc20",
612
+ hash: "wednesday_erc20",
613
+ accountId: encodeTokenAccountId(ledgerAccountId, tokenCurrency),
614
+ }),
615
+ ],
616
+ }),
617
+ getMockedOperation({
618
+ hash: "tuesday_tx",
619
+ type: "OUT",
620
+ date: new Date("2024-01-16T08:00:00Z"),
621
+ }),
622
+ ];
623
+
624
+ // thirdweb catches up with Friday's event
625
+ const lateERC20Transaction = getMockedThirdwebTransaction({
626
+ transactionHash: "friday_erc20",
627
+ address: tokenCurrency.contractAddress,
628
+ decoded: {
629
+ name: "Transfer",
630
+ signature: "Transfer(address,address,uint256)",
631
+ params: {
632
+ from: evmAddress,
633
+ to: "0xRECIPIENT",
634
+ value: "5000000",
635
+ },
636
+ },
372
637
  });
373
638
 
374
- const extra: HederaOperationExtra = {
375
- consensusTimestamp: "12345",
376
- associatedTokenId: "0.0.1001",
377
- };
639
+ mockGetContractCallResult.mockResolvedValue({
640
+ timestamp: fridayTxConsensusTimestamp,
641
+ contract_id: tokenCurrency.contractAddress,
642
+ } as any);
643
+
644
+ mockFindTransactionByContractCall.mockResolvedValue({
645
+ transaction_hash: lateERC20Transaction.transactionHash,
646
+ consensus_timestamp: fridayTxConsensusTimestamp,
647
+ } as any);
648
+
649
+ const { updatedOperations, newERC20TokenOperations } = await integrateERC20Operations({
650
+ ledgerAccountId,
651
+ address,
652
+ allOperations: fridaySyncOperations,
653
+ latestERC20Transactions: [lateERC20Transaction],
654
+ pendingOperationHashes: new Set(),
655
+ erc20OperationHashes: new Set(["wednesday_erc20"]),
656
+ });
378
657
 
379
- const patched = patchOperationWithExtra(mockedOperation, extra);
658
+ // check if friday operation got patched
659
+ const wednesdayOp = updatedOperations.find(op => op.hash === "wednesday_erc20");
660
+ const fridayOp = updatedOperations.find(
661
+ op => op.hash === lateERC20Transaction.transactionHash,
662
+ );
380
663
 
381
- expect(patched.extra).toEqual(extra);
382
- expect(patched.subOperations).toHaveLength(1);
383
- expect(patched.subOperations?.[0].extra).toEqual(extra);
664
+ expect(fridayOp).toMatchObject({
665
+ type: "FEES",
666
+ standard: "erc20",
667
+ hash: lateERC20Transaction.transactionHash,
668
+ subOperations: [
669
+ {
670
+ type: "OUT",
671
+ standard: "erc20",
672
+ },
673
+ ],
674
+ });
675
+ expect(newERC20TokenOperations).toHaveLength(1);
676
+ expect(newERC20TokenOperations).toMatchObject([
677
+ {
678
+ type: "OUT",
679
+ hash: lateERC20Transaction.transactionHash,
680
+ accountId: encodeTokenAccountId(ledgerAccountId, tokenCurrency),
681
+ },
682
+ ]);
683
+ expect(wednesdayOp).toMatchObject({
684
+ type: "FEES",
685
+ blockHash: "0xWEDNESDAY_BLOCK",
686
+ });
687
+ expect(updatedOperations[0]).toMatchObject({ hash: "saturday_tx" });
688
+ expect(updatedOperations.at(-1)).toMatchObject({ hash: "tuesday_tx" });
689
+ expect(updatedOperations).toHaveLength(fridaySyncOperations.length);
384
690
  });
385
691
  });
386
692
  });