@ledgerhq/coin-hedera 1.15.0 → 1.16.0-nightly.20251210120335

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 (186) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/lib/bridge/buildOptimisticOperation.d.ts.map +1 -1
  3. package/lib/bridge/buildOptimisticOperation.js +33 -0
  4. package/lib/bridge/buildOptimisticOperation.js.map +1 -1
  5. package/lib/bridge/getTransactionStatus.d.ts.map +1 -1
  6. package/lib/bridge/getTransactionStatus.js +54 -0
  7. package/lib/bridge/getTransactionStatus.js.map +1 -1
  8. package/lib/bridge/index.d.ts.map +1 -1
  9. package/lib/bridge/index.js +4 -2
  10. package/lib/bridge/index.js.map +1 -1
  11. package/lib/bridge/prepareTransaction.d.ts.map +1 -1
  12. package/lib/bridge/prepareTransaction.js +16 -0
  13. package/lib/bridge/prepareTransaction.js.map +1 -1
  14. package/lib/bridge/serialization.d.ts.map +1 -1
  15. package/lib/bridge/serialization.js +20 -0
  16. package/lib/bridge/serialization.js.map +1 -1
  17. package/lib/bridge/signOperation.d.ts +4 -4
  18. package/lib/bridge/signOperation.d.ts.map +1 -1
  19. package/lib/bridge/signOperation.js +10 -0
  20. package/lib/bridge/signOperation.js.map +1 -1
  21. package/lib/bridge/synchronisation.d.ts.map +1 -1
  22. package/lib/bridge/synchronisation.js +8 -0
  23. package/lib/bridge/synchronisation.js.map +1 -1
  24. package/lib/constants.d.ts +21 -1
  25. package/lib/constants.d.ts.map +1 -1
  26. package/lib/constants.js +22 -1
  27. package/lib/constants.js.map +1 -1
  28. package/lib/deviceTransactionConfig.d.ts.map +1 -1
  29. package/lib/deviceTransactionConfig.js +30 -0
  30. package/lib/deviceTransactionConfig.js.map +1 -1
  31. package/lib/errors.d.ts +9 -0
  32. package/lib/errors.d.ts.map +1 -1
  33. package/lib/errors.js +4 -1
  34. package/lib/errors.js.map +1 -1
  35. package/lib/logic/craftTransaction.d.ts +2 -2
  36. package/lib/logic/craftTransaction.d.ts.map +1 -1
  37. package/lib/logic/craftTransaction.js +42 -8
  38. package/lib/logic/craftTransaction.js.map +1 -1
  39. package/lib/logic/getBlock.d.ts.map +1 -1
  40. package/lib/logic/getBlock.js +1 -0
  41. package/lib/logic/getBlock.js.map +1 -1
  42. package/lib/logic/listOperations.d.ts.map +1 -1
  43. package/lib/logic/listOperations.js +39 -7
  44. package/lib/logic/listOperations.js.map +1 -1
  45. package/lib/logic/utils.d.ts +61 -3
  46. package/lib/logic/utils.d.ts.map +1 -1
  47. package/lib/logic/utils.js +117 -4
  48. package/lib/logic/utils.js.map +1 -1
  49. package/lib/network/api.d.ts +3 -1
  50. package/lib/network/api.d.ts.map +1 -1
  51. package/lib/network/api.js +19 -0
  52. package/lib/network/api.js.map +1 -1
  53. package/lib/preload-data.d.ts +7 -0
  54. package/lib/preload-data.d.ts.map +1 -0
  55. package/lib/preload-data.js +37 -0
  56. package/lib/preload-data.js.map +1 -0
  57. package/lib/preload.d.ts +8 -0
  58. package/lib/preload.d.ts.map +1 -0
  59. package/lib/preload.js +76 -0
  60. package/lib/preload.js.map +1 -0
  61. package/lib/test/fixtures/account.fixture.d.ts +1 -1
  62. package/lib/test/fixtures/account.fixture.d.ts.map +1 -1
  63. package/lib/test/fixtures/account.fixture.js +2 -0
  64. package/lib/test/fixtures/account.fixture.js.map +1 -1
  65. package/lib/transaction.d.ts.map +1 -1
  66. package/lib/transaction.js +34 -0
  67. package/lib/transaction.js.map +1 -1
  68. package/lib/types/alpaca.d.ts +3 -0
  69. package/lib/types/alpaca.d.ts.map +1 -1
  70. package/lib/types/bridge.d.ts +87 -3
  71. package/lib/types/bridge.d.ts.map +1 -1
  72. package/lib/types/logic.d.ts +5 -1
  73. package/lib/types/logic.d.ts.map +1 -1
  74. package/lib/types/mirror.d.ts +19 -0
  75. package/lib/types/mirror.d.ts.map +1 -1
  76. package/lib-es/bridge/buildOptimisticOperation.d.ts.map +1 -1
  77. package/lib-es/bridge/buildOptimisticOperation.js +34 -1
  78. package/lib-es/bridge/buildOptimisticOperation.js.map +1 -1
  79. package/lib-es/bridge/getTransactionStatus.d.ts.map +1 -1
  80. package/lib-es/bridge/getTransactionStatus.js +57 -3
  81. package/lib-es/bridge/getTransactionStatus.js.map +1 -1
  82. package/lib-es/bridge/index.d.ts.map +1 -1
  83. package/lib-es/bridge/index.js +4 -2
  84. package/lib-es/bridge/index.js.map +1 -1
  85. package/lib-es/bridge/prepareTransaction.d.ts.map +1 -1
  86. package/lib-es/bridge/prepareTransaction.js +15 -2
  87. package/lib-es/bridge/prepareTransaction.js.map +1 -1
  88. package/lib-es/bridge/serialization.d.ts.map +1 -1
  89. package/lib-es/bridge/serialization.js +17 -0
  90. package/lib-es/bridge/serialization.js.map +1 -1
  91. package/lib-es/bridge/signOperation.d.ts +4 -4
  92. package/lib-es/bridge/signOperation.d.ts.map +1 -1
  93. package/lib-es/bridge/signOperation.js +11 -1
  94. package/lib-es/bridge/signOperation.js.map +1 -1
  95. package/lib-es/bridge/synchronisation.d.ts.map +1 -1
  96. package/lib-es/bridge/synchronisation.js +8 -0
  97. package/lib-es/bridge/synchronisation.js.map +1 -1
  98. package/lib-es/constants.d.ts +21 -1
  99. package/lib-es/constants.d.ts.map +1 -1
  100. package/lib-es/constants.js +21 -0
  101. package/lib-es/constants.js.map +1 -1
  102. package/lib-es/deviceTransactionConfig.d.ts.map +1 -1
  103. package/lib-es/deviceTransactionConfig.js +31 -1
  104. package/lib-es/deviceTransactionConfig.js.map +1 -1
  105. package/lib-es/errors.d.ts +9 -0
  106. package/lib-es/errors.d.ts.map +1 -1
  107. package/lib-es/errors.js +3 -0
  108. package/lib-es/errors.js.map +1 -1
  109. package/lib-es/logic/craftTransaction.d.ts +2 -2
  110. package/lib-es/logic/craftTransaction.d.ts.map +1 -1
  111. package/lib-es/logic/craftTransaction.js +44 -10
  112. package/lib-es/logic/craftTransaction.js.map +1 -1
  113. package/lib-es/logic/getBlock.d.ts.map +1 -1
  114. package/lib-es/logic/getBlock.js +2 -1
  115. package/lib-es/logic/getBlock.js.map +1 -1
  116. package/lib-es/logic/listOperations.d.ts.map +1 -1
  117. package/lib-es/logic/listOperations.js +39 -7
  118. package/lib-es/logic/listOperations.js.map +1 -1
  119. package/lib-es/logic/utils.d.ts +61 -3
  120. package/lib-es/logic/utils.d.ts.map +1 -1
  121. package/lib-es/logic/utils.js +107 -4
  122. package/lib-es/logic/utils.js.map +1 -1
  123. package/lib-es/network/api.d.ts +3 -1
  124. package/lib-es/network/api.d.ts.map +1 -1
  125. package/lib-es/network/api.js +19 -0
  126. package/lib-es/network/api.js.map +1 -1
  127. package/lib-es/preload-data.d.ts +7 -0
  128. package/lib-es/preload-data.d.ts.map +1 -0
  129. package/lib-es/preload-data.js +31 -0
  130. package/lib-es/preload-data.js.map +1 -0
  131. package/lib-es/preload.d.ts +8 -0
  132. package/lib-es/preload.d.ts.map +1 -0
  133. package/lib-es/preload.js +67 -0
  134. package/lib-es/preload.js.map +1 -0
  135. package/lib-es/test/fixtures/account.fixture.d.ts +1 -1
  136. package/lib-es/test/fixtures/account.fixture.d.ts.map +1 -1
  137. package/lib-es/test/fixtures/account.fixture.js +2 -0
  138. package/lib-es/test/fixtures/account.fixture.js.map +1 -1
  139. package/lib-es/transaction.d.ts.map +1 -1
  140. package/lib-es/transaction.js +34 -0
  141. package/lib-es/transaction.js.map +1 -1
  142. package/lib-es/types/alpaca.d.ts +3 -0
  143. package/lib-es/types/alpaca.d.ts.map +1 -1
  144. package/lib-es/types/bridge.d.ts +87 -3
  145. package/lib-es/types/bridge.d.ts.map +1 -1
  146. package/lib-es/types/logic.d.ts +5 -1
  147. package/lib-es/types/logic.d.ts.map +1 -1
  148. package/lib-es/types/mirror.d.ts +19 -0
  149. package/lib-es/types/mirror.d.ts.map +1 -1
  150. package/package.json +13 -12
  151. package/src/api/index.integ.test.ts +11 -1
  152. package/src/bridge/buildOptimisticOperation.integration.test.ts +159 -4
  153. package/src/bridge/buildOptimisticOperation.ts +50 -2
  154. package/src/bridge/getTransactionStatus.test.ts +191 -21
  155. package/src/bridge/getTransactionStatus.ts +75 -1
  156. package/src/bridge/index.ts +4 -2
  157. package/src/bridge/prepareTransaction.test.ts +112 -8
  158. package/src/bridge/prepareTransaction.ts +20 -2
  159. package/src/bridge/serialization.ts +17 -0
  160. package/src/bridge/signOperation.ts +15 -5
  161. package/src/bridge/synchronisation.ts +9 -0
  162. package/src/bridge/utils.integration.test.ts +3 -10
  163. package/src/constants.ts +22 -0
  164. package/src/deviceTransactionConfig.test.ts +315 -0
  165. package/src/deviceTransactionConfig.ts +37 -1
  166. package/src/errors.ts +7 -0
  167. package/src/logic/craftTransaction.ts +70 -13
  168. package/src/logic/getBalance.test.ts +15 -16
  169. package/src/logic/getBlock.ts +2 -1
  170. package/src/logic/listOperations.test.ts +86 -29
  171. package/src/logic/listOperations.ts +46 -6
  172. package/src/logic/utils.test.ts +362 -8
  173. package/src/logic/utils.ts +158 -4
  174. package/src/network/api.test.ts +58 -6
  175. package/src/network/api.ts +25 -0
  176. package/src/network/thirdweb.test.ts +2 -2
  177. package/src/network/utils.test.ts +4 -6
  178. package/src/preload-data.ts +38 -0
  179. package/src/preload.test.ts +64 -0
  180. package/src/preload.ts +80 -0
  181. package/src/test/fixtures/account.fixture.ts +3 -1
  182. package/src/transaction.ts +42 -0
  183. package/src/types/alpaca.ts +4 -0
  184. package/src/types/bridge.ts +108 -3
  185. package/src/types/logic.ts +6 -1
  186. package/src/types/mirror.ts +21 -0
@@ -4,18 +4,24 @@ import {
4
4
  InvalidAddressBecauseDestinationIsAlsoSource,
5
5
  AmountRequired,
6
6
  NotEnoughBalance,
7
+ ClaimRewardsFeesWarning,
8
+ RecipientRequired,
7
9
  } from "@ledgerhq/errors";
8
10
  import { HEDERA_TRANSACTION_MODES } from "../constants";
9
11
  import {
10
12
  HederaInsufficientFundsForAssociation,
13
+ HederaInvalidStakingNodeIdError,
14
+ HederaNoStakingRewardsError,
11
15
  HederaRecipientEvmAddressVerificationRequired,
12
16
  HederaRecipientInvalidChecksum,
13
17
  HederaRecipientTokenAssociationRequired,
14
18
  HederaRecipientTokenAssociationUnverified,
19
+ HederaRedundantStakingNodeIdError,
15
20
  } from "../errors";
16
21
  import { getTransactionStatus } from "./getTransactionStatus";
17
22
  import * as estimateFees from "../logic/estimateFees";
18
23
  import * as logicUtils from "../logic/utils";
24
+ import * as preloadData from "../preload-data";
19
25
  import { rpcClient } from "../network/rpc";
20
26
  import { getMockedAccount, getMockedTokenAccount } from "../test/fixtures/account.fixture";
21
27
  import {
@@ -23,26 +29,28 @@ import {
23
29
  getMockedHTSTokenCurrency,
24
30
  } from "../test/fixtures/currency.fixture";
25
31
  import { getMockedTransaction } from "../test/fixtures/transaction.fixture";
26
- import type { EstimateFeesResult } from "../types";
32
+ import type { EstimateFeesResult, HederaPreloadData } from "../types";
27
33
 
28
34
  describe("getTransactionStatus", () => {
29
35
  const mockedEstimatedFee: EstimateFeesResult = { tinybars: new BigNumber(1) };
30
36
  const mockedUsdRate = new BigNumber(1);
37
+ const mockPreload = { validators: [{ nodeId: 1 }, { nodeId: 2 }] } as HederaPreloadData;
31
38
  const validRecipientAddress = "0.0.1234567";
32
39
  const validRecipientAddressWithChecksum = "0.0.1234567-ylkls";
33
40
 
34
41
  beforeEach(() => {
35
42
  jest.clearAllMocks();
36
43
 
37
- jest.spyOn(logicUtils, "getCurrencyToUSDRate").mockResolvedValueOnce(mockedUsdRate);
38
44
  jest.spyOn(estimateFees, "estimateFees").mockResolvedValueOnce(mockedEstimatedFee);
45
+ jest.spyOn(logicUtils, "getCurrencyToUSDRate").mockResolvedValueOnce(mockedUsdRate);
46
+ jest.spyOn(preloadData, "getCurrentHederaPreloadData").mockReturnValueOnce(mockPreload);
39
47
  });
40
48
 
41
49
  afterAll(() => {
42
50
  rpcClient._resetInstance();
43
51
  });
44
52
 
45
- test("coin transfer with valid recipient and sufficient balance completes successfully", async () => {
53
+ it("coin transfer with valid recipient and sufficient balance completes successfully", async () => {
46
54
  const mockedAccount = getMockedAccount({ balance: new BigNumber(1000) });
47
55
  const mockedTransaction = getMockedTransaction({
48
56
  recipient: validRecipientAddress,
@@ -57,7 +65,7 @@ describe("getTransactionStatus", () => {
57
65
  expect(result.totalSpent.isGreaterThan(100)).toBe(true);
58
66
  });
59
67
 
60
- test("hts token transfer with valid recipient and sufficient balance completes successfully", async () => {
68
+ it("hts token transfer with valid recipient and sufficient balance completes successfully", async () => {
61
69
  jest.spyOn(logicUtils, "checkAccountTokenAssociationStatus").mockResolvedValueOnce(true);
62
70
 
63
71
  const tokenCurrency = getMockedHTSTokenCurrency();
@@ -76,7 +84,7 @@ describe("getTransactionStatus", () => {
76
84
  expect(result.amount).toEqual(new BigNumber(200));
77
85
  });
78
86
 
79
- test("erc20 token transfer with valid recipient and sufficient balance completes successfully", async () => {
87
+ it("erc20 token transfer with valid recipient and sufficient balance completes successfully", async () => {
80
88
  const tokenCurrency = getMockedERC20TokenCurrency();
81
89
  const tokenAccount = getMockedTokenAccount(tokenCurrency, { balance: new BigNumber(500) });
82
90
  const account = getMockedAccount({ balance: new BigNumber(1000), subAccounts: [tokenAccount] });
@@ -95,7 +103,7 @@ describe("getTransactionStatus", () => {
95
103
  expect(result.amount).toEqual(new BigNumber(200));
96
104
  });
97
105
 
98
- test("token associate transaction with sufficient USD worth completes successfully", async () => {
106
+ it("token associate transaction with sufficient USD worth completes successfully", async () => {
99
107
  const mockedTokenCurrency = getMockedHTSTokenCurrency();
100
108
  const mockedAccount = getMockedAccount();
101
109
  const mockedTransaction = getMockedTransaction({
@@ -114,7 +122,7 @@ describe("getTransactionStatus", () => {
114
122
  expect(result.estimatedFees).toEqual(mockedEstimatedFee.tinybars);
115
123
  });
116
124
 
117
- test("recipient with checksum is supported", async () => {
125
+ it("recipient with checksum is supported", async () => {
118
126
  const mockedAccount = getMockedAccount({ balance: new BigNumber(1000) });
119
127
  const mockedTransaction = getMockedTransaction({
120
128
  recipient: validRecipientAddressWithChecksum,
@@ -127,22 +135,25 @@ describe("getTransactionStatus", () => {
127
135
  expect(result.warnings).toEqual({});
128
136
  });
129
137
 
130
- test("adds error for invalid recipient address", async () => {
138
+ it("adds error for invalid recipient address", async () => {
131
139
  const mockedAccount = getMockedAccount();
132
140
 
133
- const txWithInvalidAddress = getMockedTransaction({ recipient: "invalid_address" });
141
+ const txWithInvalidAddress1 = getMockedTransaction({ recipient: "" });
142
+ const txWithInvalidAddress2 = getMockedTransaction({ recipient: "invalid_address" });
134
143
  const txWithInvalidAddressChecksum = getMockedTransaction({ recipient: "0.0.9124531-invld" });
135
144
 
136
- const [result1, result2] = await Promise.all([
137
- getTransactionStatus(mockedAccount, txWithInvalidAddress),
145
+ const [result1, result2, result3] = await Promise.all([
146
+ getTransactionStatus(mockedAccount, txWithInvalidAddress1),
147
+ getTransactionStatus(mockedAccount, txWithInvalidAddress2),
138
148
  getTransactionStatus(mockedAccount, txWithInvalidAddressChecksum),
139
149
  ]);
140
150
 
141
- expect(result1.errors.recipient).toBeInstanceOf(InvalidAddress);
142
- expect(result2.errors.recipient).toBeInstanceOf(HederaRecipientInvalidChecksum);
151
+ expect(result1.errors.recipient).toBeInstanceOf(RecipientRequired);
152
+ expect(result2.errors.recipient).toBeInstanceOf(InvalidAddress);
153
+ expect(result3.errors.recipient).toBeInstanceOf(HederaRecipientInvalidChecksum);
143
154
  });
144
155
 
145
- test("adds error for self transfers", async () => {
156
+ it("adds error for self transfers", async () => {
146
157
  const mockedAccount = getMockedAccount();
147
158
  const mockedTransaction = getMockedTransaction({
148
159
  recipient: mockedAccount.freshAddress,
@@ -153,7 +164,7 @@ describe("getTransactionStatus", () => {
153
164
  expect(result.errors.recipient).toBeInstanceOf(InvalidAddressBecauseDestinationIsAlsoSource);
154
165
  });
155
166
 
156
- test("adds error during coin transfer with insufficient balance", async () => {
167
+ it("adds error during coin transfer with insufficient balance", async () => {
157
168
  const mockedAccount = getMockedAccount({ balance: new BigNumber(0) });
158
169
  const mockedTransaction = getMockedTransaction({
159
170
  amount: new BigNumber(100),
@@ -165,7 +176,7 @@ describe("getTransactionStatus", () => {
165
176
  expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance);
166
177
  });
167
178
 
168
- test("adds error if USD balance is too low for token association", async () => {
179
+ it("adds error if USD balance is too low for token association", async () => {
169
180
  const mockedTokenCurrency = getMockedHTSTokenCurrency();
170
181
  const mockedAccount = getMockedAccount({ balance: new BigNumber(0) });
171
182
  const mockedTransaction = getMockedTransaction({
@@ -182,7 +193,7 @@ describe("getTransactionStatus", () => {
182
193
  );
183
194
  });
184
195
 
185
- test("adds warning during token transfer if recipient has no token associated", async () => {
196
+ it("adds warning during token transfer if recipient has no token associated", async () => {
186
197
  jest.spyOn(logicUtils, "checkAccountTokenAssociationStatus").mockResolvedValueOnce(false);
187
198
 
188
199
  const mockedTokenCurrency = getMockedHTSTokenCurrency();
@@ -200,7 +211,7 @@ describe("getTransactionStatus", () => {
200
211
  );
201
212
  });
202
213
 
203
- test("adds evm address verification warning during ERC20 token transfer", async () => {
214
+ it("adds evm address verification warning during ERC20 token transfer", async () => {
204
215
  const mockedTokenCurrency = getMockedERC20TokenCurrency();
205
216
  const mockedTokenAccount = getMockedTokenAccount(mockedTokenCurrency);
206
217
  const mockedAccount = getMockedAccount({ subAccounts: [mockedTokenAccount] });
@@ -216,7 +227,7 @@ describe("getTransactionStatus", () => {
216
227
  );
217
228
  });
218
229
 
219
- test("adds warning if token association status can't be verified", async () => {
230
+ it("adds warning if token association status can't be verified", async () => {
220
231
  jest
221
232
  .spyOn(logicUtils, "checkAccountTokenAssociationStatus")
222
233
  .mockRejectedValueOnce(new HederaRecipientTokenAssociationUnverified());
@@ -236,7 +247,7 @@ describe("getTransactionStatus", () => {
236
247
  );
237
248
  });
238
249
 
239
- test("adds error during token transfer with insufficient balance", async () => {
250
+ it("adds error during token transfer with insufficient balance", async () => {
240
251
  const mockedTokenCurrency = getMockedHTSTokenCurrency();
241
252
  const mockedTokenAccount = getMockedTokenAccount(mockedTokenCurrency, {
242
253
  balance: new BigNumber(0),
@@ -253,7 +264,7 @@ describe("getTransactionStatus", () => {
253
264
  expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance);
254
265
  });
255
266
 
256
- test("adds error if amount is zero and useAllAmount is false", async () => {
267
+ it("adds error if amount is zero and useAllAmount is false", async () => {
257
268
  const mockedAccount = getMockedAccount();
258
269
  const mockedTransaction = getMockedTransaction({
259
270
  recipient: validRecipientAddress,
@@ -265,4 +276,163 @@ describe("getTransactionStatus", () => {
265
276
 
266
277
  expect(result.errors.amount).toBeInstanceOf(AmountRequired);
267
278
  });
279
+
280
+ it("delegate transaction with valid node completes successfully", async () => {
281
+ const account = getMockedAccount();
282
+ const transaction = getMockedTransaction({
283
+ mode: HEDERA_TRANSACTION_MODES.Delegate,
284
+ properties: { stakingNodeId: 1 },
285
+ });
286
+
287
+ const result = await getTransactionStatus(account, transaction);
288
+
289
+ expect(result.errors).toEqual({});
290
+ expect(result.warnings).toEqual({});
291
+ expect(result.amount).toEqual(new BigNumber(0));
292
+ });
293
+
294
+ it("adds error for delegation without staking node id", async () => {
295
+ const account = getMockedAccount();
296
+ const transaction = getMockedTransaction({
297
+ mode: HEDERA_TRANSACTION_MODES.Delegate,
298
+ properties: {} as any,
299
+ });
300
+
301
+ const result = await getTransactionStatus(account, transaction);
302
+
303
+ expect(result.errors.missingStakingNodeId).toBeInstanceOf(HederaInvalidStakingNodeIdError);
304
+ });
305
+
306
+ it("adds error for delegation with invalid staking node id", async () => {
307
+ const account = getMockedAccount();
308
+ const transaction = getMockedTransaction({
309
+ mode: HEDERA_TRANSACTION_MODES.Delegate,
310
+ properties: { stakingNodeId: 999 },
311
+ });
312
+
313
+ const result = await getTransactionStatus(account, transaction);
314
+
315
+ expect(result.errors.stakingNodeId).toBeInstanceOf(HederaInvalidStakingNodeIdError);
316
+ });
317
+
318
+ it("adds error for delegation to already delegated node", async () => {
319
+ const account = getMockedAccount({
320
+ hederaResources: {
321
+ maxAutomaticTokenAssociations: 0,
322
+ isAutoTokenAssociationEnabled: false,
323
+ delegation: {
324
+ nodeId: 1,
325
+ pendingReward: new BigNumber(0),
326
+ delegated: new BigNumber(1000),
327
+ },
328
+ },
329
+ });
330
+ const transaction = getMockedTransaction({
331
+ mode: HEDERA_TRANSACTION_MODES.Delegate,
332
+ properties: { stakingNodeId: 1 },
333
+ });
334
+
335
+ const result = await getTransactionStatus(account, transaction);
336
+
337
+ expect(result.errors.stakingNodeId).toBeInstanceOf(HederaRedundantStakingNodeIdError);
338
+ });
339
+
340
+ it("adds error during staking transfer with insufficient balance", async () => {
341
+ const mockedAccount = getMockedAccount({ balance: new BigNumber(0) });
342
+ const mockedDelegateTransaction = getMockedTransaction({
343
+ mode: HEDERA_TRANSACTION_MODES.Delegate,
344
+ properties: { stakingNodeId: 1 },
345
+ });
346
+ const mockedUndelegateTransaction = getMockedTransaction({
347
+ mode: HEDERA_TRANSACTION_MODES.Undelegate,
348
+ properties: { stakingNodeId: null },
349
+ });
350
+ const mockedRedelegateTransaction = getMockedTransaction({
351
+ mode: HEDERA_TRANSACTION_MODES.Redelegate,
352
+ properties: { stakingNodeId: 2 },
353
+ });
354
+ const mockedClaimRewardsTransaction = getMockedTransaction({
355
+ mode: HEDERA_TRANSACTION_MODES.ClaimRewards,
356
+ });
357
+
358
+ const [resultDelegate, resultUndelegate, resultRedelegate, resultClaimRewards] =
359
+ await Promise.all([
360
+ getTransactionStatus(mockedAccount, mockedDelegateTransaction),
361
+ getTransactionStatus(mockedAccount, mockedUndelegateTransaction),
362
+ getTransactionStatus(mockedAccount, mockedRedelegateTransaction),
363
+ getTransactionStatus(mockedAccount, mockedClaimRewardsTransaction),
364
+ ]);
365
+
366
+ expect(resultDelegate.errors.fee).toBeInstanceOf(NotEnoughBalance);
367
+ expect(resultUndelegate.errors.fee).toBeInstanceOf(NotEnoughBalance);
368
+ expect(resultRedelegate.errors.fee).toBeInstanceOf(NotEnoughBalance);
369
+ expect(resultClaimRewards.errors.fee).toBeInstanceOf(NotEnoughBalance);
370
+ });
371
+
372
+ it("adds error when claiming rewards with no rewards available", async () => {
373
+ const account = getMockedAccount({
374
+ hederaResources: {
375
+ maxAutomaticTokenAssociations: 0,
376
+ isAutoTokenAssociationEnabled: false,
377
+ delegation: {
378
+ nodeId: 1,
379
+ pendingReward: new BigNumber(0),
380
+ delegated: new BigNumber(1000),
381
+ },
382
+ },
383
+ });
384
+ const transaction = getMockedTransaction({
385
+ mode: HEDERA_TRANSACTION_MODES.ClaimRewards,
386
+ });
387
+
388
+ const result = await getTransactionStatus(account, transaction);
389
+
390
+ expect(result.errors.noRewardsToClaim).toBeInstanceOf(HederaNoStakingRewardsError);
391
+ });
392
+
393
+ it("adds warning when claiming rewards with fee higher than rewards", async () => {
394
+ const account = getMockedAccount({
395
+ hederaResources: {
396
+ maxAutomaticTokenAssociations: 0,
397
+ isAutoTokenAssociationEnabled: false,
398
+ delegation: {
399
+ nodeId: 1,
400
+ pendingReward: new BigNumber(10),
401
+ delegated: new BigNumber(1000),
402
+ },
403
+ },
404
+ });
405
+ const transaction = getMockedTransaction({
406
+ mode: HEDERA_TRANSACTION_MODES.ClaimRewards,
407
+ maxFee: new BigNumber(100),
408
+ });
409
+
410
+ const result = await getTransactionStatus(account, transaction);
411
+
412
+ expect(result.warnings.claimRewardsFee).toBeInstanceOf(ClaimRewardsFeesWarning);
413
+ });
414
+
415
+ it("claim rewards with sufficient rewards completes successfully", async () => {
416
+ const account = getMockedAccount({
417
+ hederaResources: {
418
+ maxAutomaticTokenAssociations: 0,
419
+ isAutoTokenAssociationEnabled: false,
420
+ delegation: {
421
+ nodeId: 1,
422
+ pendingReward: new BigNumber(100),
423
+ delegated: new BigNumber(1000),
424
+ },
425
+ },
426
+ });
427
+ const transaction = getMockedTransaction({
428
+ mode: HEDERA_TRANSACTION_MODES.ClaimRewards,
429
+ maxFee: new BigNumber(10),
430
+ });
431
+
432
+ const result = await getTransactionStatus(account, transaction);
433
+
434
+ expect(result.errors).toEqual({});
435
+ expect(result.warnings).toEqual({});
436
+ expect(result.amount).toEqual(new BigNumber(0));
437
+ });
268
438
  });
@@ -1,9 +1,11 @@
1
1
  import BigNumber from "bignumber.js";
2
+ import invariant from "invariant";
2
3
  import {
3
4
  AmountRequired,
4
5
  NotEnoughBalance,
5
6
  InvalidAddressBecauseDestinationIsAlsoSource,
6
7
  RecipientRequired,
8
+ ClaimRewardsFeesWarning,
7
9
  } from "@ledgerhq/errors";
8
10
  import type { Account, AccountBridge, TokenAccount } from "@ledgerhq/types-live";
9
11
  import { findSubAccountById } from "@ledgerhq/coin-framework/account";
@@ -14,6 +16,9 @@ import {
14
16
  HederaRecipientTokenAssociationRequired,
15
17
  HederaRecipientTokenAssociationUnverified,
16
18
  HederaRecipientEvmAddressVerificationRequired,
19
+ HederaInvalidStakingNodeIdError,
20
+ HederaRedundantStakingNodeIdError,
21
+ HederaNoStakingRewardsError,
17
22
  } from "../errors";
18
23
  import { estimateFees } from "../logic/estimateFees";
19
24
  import {
@@ -22,8 +27,15 @@ import {
22
27
  getCurrencyToUSDRate,
23
28
  checkAccountTokenAssociationStatus,
24
29
  safeParseAccountId,
30
+ isStakingTransaction,
25
31
  } from "../logic/utils";
26
- import type { Transaction, TransactionStatus, TransactionTokenAssociate } from "../types";
32
+ import { getCurrentHederaPreloadData } from "../preload-data";
33
+ import type {
34
+ HederaAccount,
35
+ Transaction,
36
+ TransactionStatus,
37
+ TransactionTokenAssociate,
38
+ } from "../types";
27
39
  import { calculateAmount } from "./utils";
28
40
 
29
41
  type Errors = Record<string, Error>;
@@ -241,6 +253,66 @@ async function handleCoinTransaction(
241
253
  };
242
254
  }
243
255
 
256
+ async function handleStakingTransaction(account: HederaAccount, transaction: Transaction) {
257
+ invariant(isStakingTransaction(transaction), "invalid transaction properties");
258
+
259
+ const errors: Record<string, Error> = {};
260
+ const warnings: Record<string, Error> = {};
261
+ const { validators } = getCurrentHederaPreloadData(account.currency);
262
+ const estimatedFees = await estimateFees({
263
+ operationType: HEDERA_OPERATION_TYPES.CryptoUpdate,
264
+ currency: account.currency,
265
+ });
266
+ const amount = BigNumber(0);
267
+ const totalSpent = amount.plus(estimatedFees.tinybars);
268
+
269
+ if (
270
+ transaction.mode === HEDERA_TRANSACTION_MODES.Delegate ||
271
+ transaction.mode === HEDERA_TRANSACTION_MODES.Redelegate
272
+ ) {
273
+ if (typeof transaction.properties?.stakingNodeId === "number") {
274
+ const isValid = validators.some(validator => {
275
+ return validator.nodeId === transaction.properties?.stakingNodeId;
276
+ });
277
+
278
+ if (!isValid) {
279
+ errors.stakingNodeId = new HederaInvalidStakingNodeIdError();
280
+ }
281
+ } else {
282
+ errors.missingStakingNodeId = new HederaInvalidStakingNodeIdError("Validator must be set");
283
+ }
284
+
285
+ if (account.hederaResources?.delegation?.nodeId === transaction.properties?.stakingNodeId) {
286
+ errors.stakingNodeId = new HederaRedundantStakingNodeIdError();
287
+ }
288
+ }
289
+
290
+ if (transaction.mode === HEDERA_TRANSACTION_MODES.ClaimRewards) {
291
+ const rewardsToClaim = account.hederaResources?.delegation?.pendingReward || new BigNumber(0);
292
+ const transactionFee = transaction.maxFee ?? new BigNumber(0);
293
+
294
+ if (rewardsToClaim.lte(0)) {
295
+ errors.noRewardsToClaim = new HederaNoStakingRewardsError();
296
+ }
297
+
298
+ if (transactionFee.gt(rewardsToClaim)) {
299
+ warnings.claimRewardsFee = new ClaimRewardsFeesWarning();
300
+ }
301
+ }
302
+
303
+ if (account.balance.isLessThan(totalSpent)) {
304
+ errors.fee = new NotEnoughBalance("");
305
+ }
306
+
307
+ return {
308
+ amount: new BigNumber(0),
309
+ estimatedFees: estimatedFees.tinybars,
310
+ totalSpent,
311
+ errors,
312
+ warnings,
313
+ };
314
+ }
315
+
244
316
  export const getTransactionStatus: AccountBridge<
245
317
  Transaction,
246
318
  Account,
@@ -258,6 +330,8 @@ export const getTransactionStatus: AccountBridge<
258
330
  return handleHTSTokenTransaction(account, subAccount, transaction);
259
331
  } else if (isERC20TokenTransaction) {
260
332
  return handleERC20TokenTransaction(account, subAccount, transaction);
333
+ } else if (isStakingTransaction(transaction)) {
334
+ return handleStakingTransaction(account, transaction);
261
335
  } else {
262
336
  return handleCoinTransaction(account, transaction);
263
337
  }
@@ -13,6 +13,7 @@ import { estimateMaxSpendable } from "./estimateMaxSpendable";
13
13
  import { getTransactionStatus } from "./getTransactionStatus";
14
14
  import { prepareTransaction } from "./prepareTransaction";
15
15
  import { receive } from "./receive";
16
+ import { getPreloadStrategy, hydrate, preload } from "../preload";
16
17
  import { buildSignOperation } from "./signOperation";
17
18
  import { getAccountShape, buildIterateResult, postSync } from "./synchronisation";
18
19
  import { assignFromAccountRaw, assignToAccountRaw } from "./serialization";
@@ -29,8 +30,9 @@ function buildCurrencyBridge(signerContext: SignerContext<HederaSigner>): Curren
29
30
  });
30
31
 
31
32
  return {
32
- preload: () => Promise.resolve({}),
33
- hydrate: () => {},
33
+ preload,
34
+ hydrate,
35
+ getPreloadStrategy,
34
36
  scanAccounts,
35
37
  };
36
38
  }
@@ -1,32 +1,136 @@
1
1
  import BigNumber from "bignumber.js";
2
2
  import { estimateFees } from "../logic/estimateFees";
3
3
  import { prepareTransaction } from "./prepareTransaction";
4
- import { getMockedAccount } from "../test/fixtures/account.fixture";
4
+ import { getMockedAccount, getMockedTokenAccount } from "../test/fixtures/account.fixture";
5
5
  import { getMockedTransaction } from "../test/fixtures/transaction.fixture";
6
6
  import * as utils from "./utils";
7
7
  import type { EstimateFeesResult } from "../types";
8
+ import { HEDERA_OPERATION_TYPES, HEDERA_TRANSACTION_MODES } from "../constants";
9
+ import {
10
+ getMockedERC20TokenCurrency,
11
+ getMockedHTSTokenCurrency,
12
+ } from "../test/fixtures/currency.fixture";
8
13
 
9
14
  jest.mock("../logic/estimateFees");
10
15
 
11
16
  describe("prepareTransaction", () => {
12
17
  const mockAccount = getMockedAccount();
13
18
  const mockTx = getMockedTransaction();
14
- const mockFeeEstimation: EstimateFeesResult = { tinybars: new BigNumber(10) };
19
+ const mockFeeEstimation: EstimateFeesResult = {
20
+ tinybars: new BigNumber(10),
21
+ gas: new BigNumber(5),
22
+ };
23
+ const mockCalculatedAmount = {
24
+ amount: new BigNumber(100),
25
+ totalSpent: new BigNumber(100),
26
+ };
15
27
 
16
28
  beforeEach(() => {
17
29
  jest.clearAllMocks();
18
30
 
19
31
  (estimateFees as jest.Mock).mockResolvedValue(mockFeeEstimation);
20
- jest
21
- .spyOn(utils, "calculateAmount")
22
- .mockResolvedValue(
23
- Promise.resolve({ amount: new BigNumber(100), totalSpent: new BigNumber(100) }),
24
- );
32
+ jest.spyOn(utils, "calculateAmount").mockResolvedValue(mockCalculatedAmount);
25
33
  });
26
34
 
27
- test("should set amount and maxFee from utils", async () => {
35
+ it("should set amount and maxFee from utils", async () => {
28
36
  const result = await prepareTransaction(mockAccount, mockTx);
37
+
29
38
  expect(result.amount).toStrictEqual(new BigNumber(100));
30
39
  expect(result.maxFee).toStrictEqual(new BigNumber(10));
31
40
  });
41
+
42
+ it("should build ContractCall estimation params with txIntent", async () => {
43
+ const mockTokenERC20 = getMockedERC20TokenCurrency();
44
+ const tokenAccount = getMockedTokenAccount(mockTokenERC20);
45
+ const accountWithToken = getMockedAccount({ subAccounts: [tokenAccount] });
46
+ const transaction = getMockedTransaction({
47
+ mode: HEDERA_TRANSACTION_MODES.Send,
48
+ subAccountId: tokenAccount.id,
49
+ amount: new BigNumber(5000),
50
+ recipient: "0.0.9999",
51
+ });
52
+
53
+ await prepareTransaction(accountWithToken, transaction);
54
+
55
+ expect(estimateFees).toHaveBeenCalledWith({
56
+ operationType: HEDERA_OPERATION_TYPES.ContractCall,
57
+ txIntent: {
58
+ intentType: "transaction",
59
+ type: HEDERA_TRANSACTION_MODES.Send,
60
+ asset: {
61
+ type: mockTokenERC20.tokenType,
62
+ assetReference: mockTokenERC20.contractAddress,
63
+ assetOwner: accountWithToken.freshAddress,
64
+ },
65
+ amount: BigInt(5000),
66
+ sender: accountWithToken.freshAddress,
67
+ recipient: transaction.recipient,
68
+ },
69
+ });
70
+ });
71
+
72
+ it("should set gasLimit for ERC20 transactions when gas is estimated", async () => {
73
+ const mockTokenERC20 = getMockedERC20TokenCurrency();
74
+ const tokenAccount = getMockedTokenAccount(mockTokenERC20);
75
+ const accountWithToken = getMockedAccount({ subAccounts: [tokenAccount] });
76
+ const transaction = getMockedTransaction({
77
+ mode: HEDERA_TRANSACTION_MODES.Send,
78
+ subAccountId: tokenAccount.id,
79
+ });
80
+
81
+ const result = await prepareTransaction(accountWithToken, transaction);
82
+
83
+ expect(result).toMatchObject({
84
+ gasLimit: mockFeeEstimation.gas,
85
+ });
86
+ });
87
+
88
+ it("should use TokenTransfer operation type for HTS tokens", async () => {
89
+ const mockTokenHTS = getMockedHTSTokenCurrency();
90
+ const tokenAccount = getMockedTokenAccount(mockTokenHTS);
91
+ const accountWithToken = getMockedAccount({ subAccounts: [tokenAccount] });
92
+ const transaction = getMockedTransaction({
93
+ mode: HEDERA_TRANSACTION_MODES.Send,
94
+ subAccountId: tokenAccount.id,
95
+ });
96
+
97
+ await prepareTransaction(accountWithToken, transaction);
98
+
99
+ expect(estimateFees).toHaveBeenCalledWith({
100
+ currency: accountWithToken.currency,
101
+ operationType: HEDERA_OPERATION_TYPES.TokenTransfer,
102
+ });
103
+ });
104
+
105
+ it("should use TokenAssociate operation type", async () => {
106
+ const mockTokenHTS = getMockedHTSTokenCurrency();
107
+ const transaction = getMockedTransaction({
108
+ mode: HEDERA_TRANSACTION_MODES.TokenAssociate,
109
+ properties: {
110
+ token: mockTokenHTS,
111
+ },
112
+ });
113
+
114
+ await prepareTransaction(mockAccount, transaction);
115
+
116
+ expect(estimateFees).toHaveBeenCalledWith({
117
+ currency: mockAccount.currency,
118
+ operationType: HEDERA_OPERATION_TYPES.TokenAssociate,
119
+ });
120
+ });
121
+
122
+ it("should use CryptoTransfer operation type for native transfers", async () => {
123
+ const transaction = getMockedTransaction({
124
+ mode: HEDERA_TRANSACTION_MODES.Send,
125
+ amount: new BigNumber(1000000),
126
+ recipient: "0.0.12345",
127
+ });
128
+
129
+ await prepareTransaction(mockAccount, transaction);
130
+
131
+ expect(estimateFees).toHaveBeenCalledWith({
132
+ currency: mockAccount.currency,
133
+ operationType: HEDERA_OPERATION_TYPES.CryptoTransfer,
134
+ });
135
+ });
32
136
  });
@@ -1,8 +1,14 @@
1
+ import BigNumber from "bignumber.js";
1
2
  import { findSubAccountById } from "@ledgerhq/coin-framework/account/helpers";
3
+ import { getEnv } from "@ledgerhq/live-env";
2
4
  import type { AccountBridge } from "@ledgerhq/types-live";
3
- import { HEDERA_OPERATION_TYPES, HEDERA_TRANSACTION_MODES } from "../constants";
5
+ import {
6
+ HEDERA_OPERATION_TYPES,
7
+ HEDERA_TRANSACTION_MODES,
8
+ MAP_STAKING_MODE_TO_MEMO,
9
+ } from "../constants";
4
10
  import { estimateFees } from "../logic/estimateFees";
5
- import { isTokenAssociateTransaction } from "../logic/utils";
11
+ import { isTokenAssociateTransaction, isStakingTransaction } from "../logic/utils";
6
12
  import type { EstimateFeesParams, Transaction } from "../types";
7
13
  import { calculateAmount } from "./utils";
8
14
 
@@ -32,6 +38,8 @@ export const prepareTransaction: AccountBridge<Transaction>["prepareTransaction"
32
38
  operationType = HEDERA_OPERATION_TYPES.TokenTransfer;
33
39
  } else if (isERC20TokenTransaction) {
34
40
  operationType = HEDERA_OPERATION_TYPES.ContractCall;
41
+ } else if (isStakingTransaction(transaction)) {
42
+ operationType = HEDERA_OPERATION_TYPES.CryptoUpdate;
35
43
  } else {
36
44
  operationType = HEDERA_OPERATION_TYPES.CryptoTransfer;
37
45
  }
@@ -79,5 +87,15 @@ export const prepareTransaction: AccountBridge<Transaction>["prepareTransaction"
79
87
  transaction.gasLimit = estimatedFees.gas;
80
88
  }
81
89
 
90
+ if (isStakingTransaction(transaction)) {
91
+ transaction.memo = MAP_STAKING_MODE_TO_MEMO[transaction.mode];
92
+
93
+ // claiming staking rewards is triggered by sending 1 tinybar to staking reward account
94
+ if (transaction.mode === HEDERA_TRANSACTION_MODES.ClaimRewards) {
95
+ transaction.recipient = getEnv("HEDERA_CLAIM_REWARDS_RECIPIENT_ACCOUNT_ID");
96
+ transaction.amount = new BigNumber(1);
97
+ }
98
+ }
99
+
82
100
  return transaction;
83
101
  };