@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
@@ -2,6 +2,7 @@ import BigNumber from "bignumber.js";
2
2
  import invariant from "invariant";
3
3
  import {
4
4
  AccountId,
5
+ AccountUpdateTransaction,
5
6
  ContractExecuteTransaction,
6
7
  ContractFunctionParameters,
7
8
  ContractId,
@@ -13,7 +14,7 @@ import {
13
14
  import type { FeeEstimation, TransactionIntent } from "@ledgerhq/coin-framework/api/index";
14
15
  import { DEFAULT_GAS_LIMIT, HEDERA_TRANSACTION_MODES } from "../constants";
15
16
  import type { HederaMemo, HederaTxData } from "../types";
16
- import { serializeTransaction } from "./utils";
17
+ import { hasSpecificIntentData, serializeTransaction } from "./utils";
17
18
 
18
19
  // avoid "sign" prompt loop by having only one node (one transaction)
19
20
  // https://github.com/LedgerHQ/ledger-live/pull/72/commits/1e942687d4301660e43e0c4b5419fcfa2733b290
@@ -55,6 +56,14 @@ interface BuilderTokenAssociateTransaction extends BuilderCommonTransactionField
55
56
  tokenId: string;
56
57
  }
57
58
 
59
+ interface BuilderUpdateAccountTransaction extends BuilderCommonTransactionFields {
60
+ type:
61
+ | HEDERA_TRANSACTION_MODES.Delegate
62
+ | HEDERA_TRANSACTION_MODES.Undelegate
63
+ | HEDERA_TRANSACTION_MODES.Redelegate;
64
+ stakingNodeId: number | null | undefined;
65
+ }
66
+
58
67
  async function buildUnsignedCoinTransaction({
59
68
  account,
60
69
  transaction,
@@ -159,13 +168,42 @@ async function buildTokenAssociateTransaction({
159
168
  return tx.freeze();
160
169
  }
161
170
 
171
+ async function buildUnsignedUpdateAccountTransaction({
172
+ account,
173
+ transaction,
174
+ }: {
175
+ account: BuilderOperator;
176
+ transaction: BuilderUpdateAccountTransaction;
177
+ }): Promise<AccountUpdateTransaction> {
178
+ const accountId = account.accountId;
179
+
180
+ const tx = new AccountUpdateTransaction()
181
+ .setNodeAccountIds([new AccountId(3)])
182
+ .setTransactionId(TransactionId.generate(accountId))
183
+ .setTransactionMemo(transaction.memo ?? "")
184
+ .setAccountId(accountId);
185
+
186
+ if (transaction.maxFee) {
187
+ tx.setMaxTransactionFee(Hbar.fromTinybars(transaction.maxFee.toNumber()));
188
+ }
189
+
190
+ if (typeof transaction.stakingNodeId === "number") {
191
+ tx.setStakedNodeId(transaction.stakingNodeId);
192
+ }
193
+
194
+ if (transaction.stakingNodeId === null) {
195
+ tx.clearStakedNodeId();
196
+ }
197
+
198
+ return tx.freeze();
199
+ }
200
+
162
201
  export async function craftTransaction(
163
202
  txIntent: TransactionIntent<HederaMemo, HederaTxData>,
164
203
  customFees?: FeeEstimation,
165
204
  ) {
166
- const account = {
167
- accountId: txIntent.sender,
168
- };
205
+ const account = { accountId: txIntent.sender };
206
+ const maxFee = customFees ? new BigNumber(customFees.value.toString()) : undefined;
169
207
 
170
208
  let tx;
171
209
 
@@ -179,7 +217,7 @@ export async function craftTransaction(
179
217
  type: txIntent.type,
180
218
  tokenId: txIntent.asset.assetReference,
181
219
  memo: txIntent.memo.value,
182
- maxFee: customFees ? new BigNumber(customFees.value.toString()) : undefined,
220
+ maxFee,
183
221
  },
184
222
  });
185
223
  } else if (txIntent.type === HEDERA_TRANSACTION_MODES.Send && txIntent.asset.type === "hts") {
@@ -195,17 +233,16 @@ export async function craftTransaction(
195
233
  amount,
196
234
  recipient: txIntent.recipient,
197
235
  memo: txIntent.memo.value,
198
- maxFee: customFees ? new BigNumber(customFees.value.toString()) : undefined,
236
+ maxFee,
199
237
  },
200
238
  });
201
239
  } else if (txIntent.type === HEDERA_TRANSACTION_MODES.Send && txIntent.asset.type === "erc20") {
202
240
  invariant("assetReference" in txIntent.asset, "hedera: no assetReference in token transfer");
203
241
 
204
242
  const amount = new BigNumber(txIntent.amount.toString());
205
- const gasLimit =
206
- "data" in txIntent && txIntent.data.gasLimit
207
- ? new BigNumber(txIntent.data.gasLimit.toString())
208
- : DEFAULT_GAS_LIMIT;
243
+ const gasLimit = hasSpecificIntentData(txIntent, "erc20")
244
+ ? new BigNumber(txIntent.data.gasLimit.toString())
245
+ : DEFAULT_GAS_LIMIT;
209
246
 
210
247
  tx = await buildUnsignedERC20TokenTransaction({
211
248
  account,
@@ -215,11 +252,31 @@ export async function craftTransaction(
215
252
  amount,
216
253
  recipient: txIntent.recipient,
217
254
  memo: txIntent.memo.value,
218
- maxFee: customFees ? new BigNumber(customFees.value.toString()) : undefined,
255
+ maxFee,
219
256
  gasLimit,
220
257
  },
221
258
  });
222
- } else {
259
+ } else if (
260
+ txIntent.type === HEDERA_TRANSACTION_MODES.Redelegate ||
261
+ txIntent.type === HEDERA_TRANSACTION_MODES.Undelegate ||
262
+ txIntent.type === HEDERA_TRANSACTION_MODES.Delegate
263
+ ) {
264
+ const stakingNodeId = hasSpecificIntentData(txIntent, "staking")
265
+ ? txIntent.data.stakingNodeId
266
+ : undefined;
267
+
268
+ tx = await buildUnsignedUpdateAccountTransaction({
269
+ account,
270
+ transaction: {
271
+ type: txIntent.type,
272
+ memo: txIntent.memo.value,
273
+ maxFee,
274
+ stakingNodeId,
275
+ },
276
+ });
277
+ }
278
+ // HEDERA_TRANSACTION_MODES.ClaimRewards is just a coin transfer that triggers staking rewards claim
279
+ else {
223
280
  const amount = new BigNumber(txIntent.amount.toString());
224
281
 
225
282
  tx = await buildUnsignedCoinTransaction({
@@ -229,7 +286,7 @@ export async function craftTransaction(
229
286
  amount,
230
287
  recipient: txIntent.recipient,
231
288
  memo: txIntent.memo.value,
232
- maxFee: customFees ? new BigNumber(customFees.value.toString()) : undefined,
289
+ maxFee,
233
290
  },
234
291
  });
235
292
  }
@@ -29,7 +29,6 @@ describe("getBalance", () => {
29
29
  expect(apiClient.getAccount).toHaveBeenCalledWith(address);
30
30
  expect(apiClient.getAccountTokens).toHaveBeenCalledTimes(1);
31
31
  expect(apiClient.getAccountTokens).toHaveBeenCalledWith(address);
32
- expect(result).toHaveLength(1);
33
32
  expect(result).toEqual([
34
33
  {
35
34
  asset: { type: "native" },
@@ -80,7 +79,6 @@ describe("getBalance", () => {
80
79
  expect(apiClient.getAccountTokens).toHaveBeenCalledWith(address);
81
80
  expect(findTokenByAddressInCurrencyMock).toHaveBeenCalledTimes(1);
82
81
  expect(findTokenByAddressInCurrencyMock).toHaveBeenCalledWith("0.0.7890", "hedera");
83
- expect(result).toHaveLength(2);
84
82
  expect(result).toEqual(
85
83
  expect.arrayContaining([
86
84
  {
@@ -150,21 +148,22 @@ describe("getBalance", () => {
150
148
 
151
149
  const result = await getBalance(mockCurrency, address);
152
150
 
153
- expect(result).toHaveLength(2);
154
- expect(result[0]).toEqual({
155
- asset: { type: "native" },
156
- value: BigInt("1000000000"),
157
- });
158
- expect(result[1]).toEqual({
159
- value: BigInt("5000"),
160
- asset: {
161
- type: mockTokenHTS.tokenType,
162
- assetReference: mockTokenHTS.contractAddress,
163
- assetOwner: address,
164
- name: mockTokenHTS.name,
165
- unit: mockTokenHTS.units[0],
151
+ expect(result).toEqual([
152
+ {
153
+ asset: { type: "native" },
154
+ value: BigInt("1000000000"),
166
155
  },
167
- });
156
+ {
157
+ value: BigInt("5000"),
158
+ asset: {
159
+ type: mockTokenHTS.tokenType,
160
+ assetReference: mockTokenHTS.contractAddress,
161
+ assetOwner: address,
162
+ name: mockTokenHTS.name,
163
+ unit: mockTokenHTS.units[0],
164
+ },
165
+ },
166
+ ]);
168
167
  });
169
168
 
170
169
  it("should throw when failing to getAccount data", async () => {
@@ -7,7 +7,7 @@ import type {
7
7
  import { getBlockInfo } from "./getBlockInfo";
8
8
  import { apiClient } from "../network/api";
9
9
  import type { HederaMirrorCoinTransfer, HederaMirrorTokenTransfer } from "../types";
10
- import { getTimestampRangeFromBlockHeight } from "./utils";
10
+ import { getMemoFromBase64, getTimestampRangeFromBlockHeight } from "./utils";
11
11
 
12
12
  function toHederaAsset(
13
13
  mirrorTransfer: HederaMirrorCoinTransfer | HederaMirrorTokenTransfer,
@@ -64,6 +64,7 @@ export async function getBlock(height: number): Promise<Block> {
64
64
  operations,
65
65
  fees: BigInt(tx.charged_tx_fee),
66
66
  feesPayer: payerAccount,
67
+ details: { memo: getMemoFromBase64(tx.memo_base64) },
67
68
  };
68
69
  });
69
70
 
@@ -1,11 +1,14 @@
1
+ import BigNumber from "bignumber.js";
2
+ import { setupMockCryptoAssetsStore } from "@ledgerhq/cryptoassets/cal-client/test-helpers";
1
3
  import { encodeTokenAccountId } from "@ledgerhq/coin-framework/account/accountId";
2
4
  import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
3
5
  import type { Pagination } from "@ledgerhq/coin-framework/api/types";
6
+ import { getEnv } from "@ledgerhq/live-env";
4
7
  import { listOperations } from "./listOperations";
5
8
  import { apiClient } from "../network/api";
6
9
  import { getMockedCurrency } from "../test/fixtures/currency.fixture";
10
+ import type { HederaMirrorTransaction } from "../types";
7
11
  import * as utils from "./utils";
8
- import { setupMockCryptoAssetsStore } from "@ledgerhq/cryptoassets/cal-client/test-helpers";
9
12
 
10
13
  setupMockCryptoAssetsStore();
11
14
  jest.mock("@ledgerhq/coin-framework/account/accountId");
@@ -73,7 +76,7 @@ describe("listOperations", () => {
73
76
  order: "desc",
74
77
  };
75
78
 
76
- const mockTransactions = [
79
+ const mockTransactions: Partial<HederaMirrorTransaction>[] = [
77
80
  {
78
81
  consensus_timestamp: "1625097600.000000000",
79
82
  transaction_hash: "hash1",
@@ -81,9 +84,10 @@ describe("listOperations", () => {
81
84
  result: "SUCCESS",
82
85
  memo_base64: "test-memo",
83
86
  token_transfers: [],
87
+ staking_reward_transfers: [],
84
88
  transfers: [
85
- { account: address, amount: "-1000000" },
86
- { account: "0.0.67890", amount: "1000000" },
89
+ { account: address, amount: -1000000 },
90
+ { account: "0.0.67890", amount: 1000000 },
87
91
  ],
88
92
  name: "CRYPTOTRANSFER",
89
93
  },
@@ -105,9 +109,8 @@ describe("listOperations", () => {
105
109
  useSyntheticBlocks: false,
106
110
  });
107
111
 
108
- expect(result.coinOperations).toHaveLength(1);
109
112
  expect(result.tokenOperations).toEqual([]);
110
-
113
+ expect(result.coinOperations).toHaveLength(1);
111
114
  expect(result.coinOperations).toMatchObject([
112
115
  {
113
116
  type: "OUT",
@@ -144,16 +147,17 @@ describe("listOperations", () => {
144
147
  units: [{ name: "TT", code: "tt", magnitude: 6 }],
145
148
  };
146
149
 
147
- const mockTransactions = [
150
+ const mockTransactions: Partial<HederaMirrorTransaction>[] = [
148
151
  {
149
152
  consensus_timestamp: "1625097600.000000000",
150
153
  transaction_hash: "hash1",
151
154
  charged_tx_fee: 500000,
152
155
  result: "SUCCESS",
153
156
  token_transfers: [
154
- { token_id: tokenId, account: address, amount: "-1000" },
155
- { token_id: tokenId, account: "0.0.67890", amount: "1000" },
157
+ { token_id: tokenId, account: address, amount: -1000 },
158
+ { token_id: tokenId, account: "0.0.67890", amount: 1000 },
156
159
  ],
160
+ staking_reward_transfers: [],
157
161
  transfers: [],
158
162
  name: "CRYPTOTRANSFER",
159
163
  },
@@ -180,16 +184,12 @@ describe("listOperations", () => {
180
184
  useSyntheticBlocks: false,
181
185
  });
182
186
 
183
- expect(result.coinOperations).toHaveLength(1);
184
- expect(result.tokenOperations).toHaveLength(1);
185
-
186
187
  expect(result.coinOperations).toMatchObject([
187
188
  {
188
189
  type: "FEES",
189
190
  fee: expect.any(Object),
190
191
  },
191
192
  ]);
192
-
193
193
  expect(result.tokenOperations).toMatchObject([
194
194
  {
195
195
  type: "OUT",
@@ -216,14 +216,15 @@ describe("listOperations", () => {
216
216
  order: "desc",
217
217
  };
218
218
 
219
- const mockTransactions = [
219
+ const mockTransactions: Partial<HederaMirrorTransaction>[] = [
220
220
  {
221
221
  consensus_timestamp: "1625097600.000000000",
222
222
  transaction_hash: "hash1",
223
223
  charged_tx_fee: 500000,
224
224
  result: "SUCCESS",
225
225
  token_transfers: [],
226
- transfers: [{ account: address, amount: "-500000" }],
226
+ staking_reward_transfers: [],
227
+ transfers: [{ account: address, amount: -500000 }],
227
228
  name: "TOKENASSOCIATE",
228
229
  },
229
230
  ];
@@ -244,9 +245,7 @@ describe("listOperations", () => {
244
245
  useSyntheticBlocks: false,
245
246
  });
246
247
 
247
- expect(result.coinOperations).toHaveLength(1);
248
- expect(result.tokenOperations).toHaveLength(0);
249
-
248
+ expect(result.tokenOperations).toEqual([]);
250
249
  expect(result.coinOperations).toMatchObject([
251
250
  {
252
251
  type: "ASSOCIATE_TOKEN",
@@ -273,16 +272,17 @@ describe("listOperations", () => {
273
272
  order: "desc",
274
273
  };
275
274
 
276
- const mockTransactions = [
275
+ const mockTransactions: Partial<HederaMirrorTransaction>[] = [
277
276
  {
278
277
  consensus_timestamp: "1625097600.000000000",
279
278
  transaction_hash: "hash1",
280
279
  charged_tx_fee: 500000,
281
280
  result: "SUCCESS",
282
281
  token_transfers: [
283
- { token_id: tokenId, account: address, amount: "-1000" },
284
- { token_id: tokenId, account: "0.0.67890", amount: "1000" },
282
+ { token_id: tokenId, account: address, amount: -1000 },
283
+ { token_id: tokenId, account: "0.0.67890", amount: 1000 },
285
284
  ],
285
+ staking_reward_transfers: [],
286
286
  transfers: [],
287
287
  name: "CRYPTOTRANSFER",
288
288
  },
@@ -309,8 +309,8 @@ describe("listOperations", () => {
309
309
  useSyntheticBlocks: false,
310
310
  });
311
311
 
312
- expect(result.coinOperations).toHaveLength(0);
313
- expect(result.tokenOperations).toHaveLength(0);
312
+ expect(result.coinOperations).toEqual([]);
313
+ expect(result.tokenOperations).toEqual([]);
314
314
  });
315
315
 
316
316
  it("should use pagination parameters correctly", async () => {
@@ -358,17 +358,18 @@ describe("listOperations", () => {
358
358
  order: "desc",
359
359
  };
360
360
 
361
- const mockTransactions = [
361
+ const mockTransactions: Partial<HederaMirrorTransaction>[] = [
362
362
  {
363
363
  consensus_timestamp: "1625097600.000000000",
364
364
  transaction_hash: "hash1",
365
365
  charged_tx_fee: 500000,
366
366
  result: "INVALID_SIGNATURE",
367
- memo_base64: null,
367
+ memo_base64: "",
368
368
  token_transfers: [],
369
+ staking_reward_transfers: [],
369
370
  transfers: [
370
- { account: address, amount: "-1000000" },
371
- { account: "0.0.67890", amount: "1000000" },
371
+ { account: address, amount: -1000000 },
372
+ { account: "0.0.67890", amount: 1000000 },
372
373
  ],
373
374
  name: "CRYPTOTRANSFER",
374
375
  },
@@ -390,7 +391,63 @@ describe("listOperations", () => {
390
391
  useSyntheticBlocks: false,
391
392
  });
392
393
 
393
- expect(result.coinOperations).toHaveLength(1);
394
- expect(result.coinOperations[0].hasFailed).toBe(true);
394
+ expect(result.coinOperations).toMatchObject([{ hasFailed: true }]);
395
+ });
396
+
397
+ it("should create REWARD operation when staking rewards are present", async () => {
398
+ const address = "0.0.1234567";
399
+ const mockCurrency = getMockedCurrency();
400
+ const pagination: Pagination = {
401
+ minHeight: 0,
402
+ limit: 10,
403
+ order: "desc",
404
+ };
405
+ const mockTransaction: Partial<HederaMirrorTransaction> = {
406
+ consensus_timestamp: "1625097600.000000000",
407
+ transaction_hash: "hash1",
408
+ charged_tx_fee: 500000,
409
+ result: "SUCCESS",
410
+ memo_base64: "",
411
+ token_transfers: [],
412
+ staking_reward_transfers: [{ account: address, amount: 1000000 }],
413
+ transfers: [{ account: address, amount: -500000 }],
414
+ name: "CRYPTOTRANSFER",
415
+ };
416
+
417
+ (apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
418
+ transactions: [mockTransaction],
419
+ nextCursor: null,
420
+ });
421
+
422
+ const result = await listOperations({
423
+ currency: mockCurrency,
424
+ address,
425
+ pagination,
426
+ mirrorTokens: [],
427
+ fetchAllPages: true,
428
+ skipFeesForTokenOperations: false,
429
+ useEncodedHash: false,
430
+ useSyntheticBlocks: false,
431
+ });
432
+
433
+ const rewardTimestamp = result.coinOperations[0].date.getTime();
434
+ const mainTimestamp = result.coinOperations[1].date.getTime();
435
+
436
+ expect(result.tokenOperations).toEqual([]);
437
+ expect(rewardTimestamp).toBe(mainTimestamp + 1);
438
+ expect(result.coinOperations).toMatchObject([
439
+ {
440
+ type: "REWARD",
441
+ hash: `${mockTransaction.transaction_hash}-staking-reward`,
442
+ value: new BigNumber(1000000),
443
+ fee: new BigNumber(0),
444
+ senders: [getEnv("HEDERA_STAKING_REWARD_ACCOUNT_ID")],
445
+ recipients: [address],
446
+ },
447
+ {
448
+ type: "OUT",
449
+ hash: mockTransaction.transaction_hash,
450
+ },
451
+ ]);
395
452
  });
396
453
  });
@@ -1,4 +1,5 @@
1
1
  import BigNumber from "bignumber.js";
2
+ import { getEnv } from "@ledgerhq/live-env";
2
3
  import type { Operation, OperationType } from "@ledgerhq/types-live";
3
4
  import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
4
5
  import type { Pagination } from "@ledgerhq/coin-framework/api/types";
@@ -13,6 +14,7 @@ import { base64ToUrlSafeBase64, getMemoFromBase64, getSyntheticBlock } from "./u
13
14
  const txNameToCustomOperationType: Record<string, OperationType> = {
14
15
  TOKENASSOCIATE: "ASSOCIATE_TOKEN",
15
16
  CONTRACTCALL: "CONTRACT_CALL",
17
+ CRYPTOUPDATEACCOUNT: "UPDATE_ACCOUNT",
16
18
  };
17
19
 
18
20
  function getCommonOperationData(
@@ -133,14 +135,27 @@ function processTransfers({
133
135
  ledgerAccountId: string;
134
136
  commonData: ReturnType<typeof getCommonOperationData>;
135
137
  mirrorTokens: HederaMirrorToken[];
136
- }): Operation<HederaOperationExtra> | null {
138
+ }): Operation<HederaOperationExtra>[] {
139
+ const coinOperations: Operation<HederaOperationExtra>[] = [];
137
140
  const transfers = rawTx.transfers ?? [];
138
- if (transfers.length === 0) return null;
141
+
142
+ if (transfers.length === 0) {
143
+ return [];
144
+ }
139
145
 
140
146
  const { type, value, senders, recipients } = parseTransfers(transfers, address);
141
147
  const { hash, fee, timestamp, blockHeight, blockHash, hasFailed } = commonData;
142
148
  const extra = { ...commonData.extra };
143
149
  const operationType = txNameToCustomOperationType[rawTx.name] ?? type;
150
+ const stakingReward = rawTx.staking_reward_transfers.reduce((acc, transfer) => {
151
+ const transferAmount = new BigNumber(transfer.amount);
152
+
153
+ if (transfer.account === address) {
154
+ acc = acc.plus(transferAmount);
155
+ }
156
+
157
+ return acc;
158
+ }, new BigNumber(0));
144
159
 
145
160
  // try to enrich ASSOCIATE_TOKEN operation with extra.associatedTokenId
146
161
  // this value is used by custom OperationDetails components in Hedera family
@@ -155,7 +170,30 @@ function processTransfers({
155
170
  }
156
171
  }
157
172
 
158
- return {
173
+ // add REWARD operation representing staking reward transfers
174
+ if (stakingReward.gt(0)) {
175
+ const stakingRewardHash = `${hash}-staking-reward`;
176
+ const stakingRewardType: OperationType = "REWARD";
177
+ // offset timestamp by +1ms to ensure it appears just before the operation that triggered it
178
+ const stakingRewardTimestamp = new Date(timestamp.getTime() + 1);
179
+
180
+ coinOperations.push({
181
+ id: encodeOperationId(ledgerAccountId, stakingRewardHash, stakingRewardType),
182
+ accountId: ledgerAccountId,
183
+ type: stakingRewardType,
184
+ value: stakingReward,
185
+ recipients: [address],
186
+ senders: [getEnv("HEDERA_STAKING_REWARD_ACCOUNT_ID")],
187
+ hash: stakingRewardHash,
188
+ fee: new BigNumber(0),
189
+ date: stakingRewardTimestamp,
190
+ blockHeight,
191
+ blockHash,
192
+ extra,
193
+ });
194
+ }
195
+
196
+ coinOperations.push({
159
197
  id: encodeOperationId(ledgerAccountId, hash, operationType),
160
198
  accountId: ledgerAccountId,
161
199
  type: operationType,
@@ -169,7 +207,9 @@ function processTransfers({
169
207
  blockHash,
170
208
  hasFailed,
171
209
  extra,
172
- };
210
+ });
211
+
212
+ return coinOperations;
173
213
  }
174
214
 
175
215
  export async function listOperations({
@@ -231,7 +271,7 @@ export async function listOperations({
231
271
 
232
272
  // process regular transfers only if there were no token transfers
233
273
  if (!tokenResult) {
234
- const coinOperation = processTransfers({
274
+ const newCoinOperations = processTransfers({
235
275
  rawTx,
236
276
  address,
237
277
  ledgerAccountId,
@@ -239,7 +279,7 @@ export async function listOperations({
239
279
  mirrorTokens,
240
280
  });
241
281
 
242
- if (coinOperation) coinOperations.push(coinOperation);
282
+ coinOperations.push(...newCoinOperations);
243
283
  }
244
284
  }
245
285