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

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
@@ -1,19 +1,33 @@
1
1
  import BigNumber from "bignumber.js";
2
2
  import { createHash } from "crypto";
3
- import { Transaction, TransactionId } from "@hashgraph/sdk";
4
- import type { AssetInfo } from "@ledgerhq/coin-framework/api/types";
3
+ import { Transaction as SDKTransaction, TransactionId } from "@hashgraph/sdk";
4
+ import type { AssetInfo, TransactionIntent } from "@ledgerhq/coin-framework/api/types";
5
5
  import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies";
6
6
  import { InvalidAddress } from "@ledgerhq/errors";
7
- import { HEDERA_TRANSACTION_MODES, SYNTHETIC_BLOCK_WINDOW_SECONDS } from "../constants";
7
+ import { getEnv, setEnv } from "@ledgerhq/live-env";
8
+ import {
9
+ HEDERA_OPERATION_TYPES,
10
+ HEDERA_TRANSACTION_MODES,
11
+ SYNTHETIC_BLOCK_WINDOW_SECONDS,
12
+ } from "../constants";
8
13
  import { HederaRecipientInvalidChecksum } from "../errors";
9
14
  import { apiClient } from "../network/api";
10
15
  import { rpcClient } from "../network/rpc";
16
+ import * as preloadData from "../preload-data";
11
17
  import { getMockedOperation } from "../test/fixtures/operation.fixture";
12
18
  import {
13
19
  getMockedERC20TokenCurrency,
14
20
  getMockedHTSTokenCurrency,
15
21
  } from "../test/fixtures/currency.fixture";
16
22
  import { getMockedAccount, getMockedTokenAccount } from "../test/fixtures/account.fixture";
23
+ import type {
24
+ HederaAccount,
25
+ HederaMemo,
26
+ HederaPreloadData,
27
+ HederaTxData,
28
+ HederaValidator,
29
+ Transaction,
30
+ } from "../types";
17
31
  import {
18
32
  serializeSignature,
19
33
  deserializeSignature,
@@ -35,15 +49,36 @@ import {
35
49
  formatTransactionId,
36
50
  getTimestampRangeFromBlockHeight,
37
51
  getBlockHash,
52
+ isStakingTransaction,
53
+ extractCompanyFromNodeDescription,
54
+ sortValidators,
55
+ getValidatorFromAccount,
56
+ getDefaultValidator,
57
+ getDelegationStatus,
58
+ filterValidatorBySearchTerm,
59
+ hasSpecificIntentData,
60
+ getChecksum,
61
+ mapIntentToSDKOperation,
62
+ getOperationDetailsExtraFields,
38
63
  } from "./utils";
39
64
 
40
65
  jest.mock("../network/api");
41
66
 
42
67
  describe("logic utils", () => {
68
+ let oldStakingLedgerNodeIdEnv: number;
69
+
43
70
  beforeEach(() => {
44
71
  jest.clearAllMocks();
45
72
  });
46
73
 
74
+ afterEach(() => {
75
+ setEnv("HEDERA_STAKING_LEDGER_NODE_ID", oldStakingLedgerNodeIdEnv);
76
+ });
77
+
78
+ beforeAll(() => {
79
+ oldStakingLedgerNodeIdEnv = getEnv("HEDERA_STAKING_LEDGER_NODE_ID");
80
+ });
81
+
47
82
  afterAll(() => {
48
83
  rpcClient._resetInstance();
49
84
  });
@@ -66,7 +101,7 @@ describe("logic utils", () => {
66
101
 
67
102
  describe("transaction serialization", () => {
68
103
  beforeEach(() => {
69
- jest.spyOn(Transaction, "fromBytes");
104
+ jest.spyOn(SDKTransaction, "fromBytes");
70
105
  });
71
106
 
72
107
  afterEach(() => {
@@ -76,7 +111,7 @@ describe("logic utils", () => {
76
111
  it("should serialize a transaction to hex", () => {
77
112
  const mockTransaction = {
78
113
  toBytes: jest.fn().mockReturnValue(Buffer.from([10, 20, 30, 40, 50])),
79
- } as unknown as Transaction;
114
+ } as unknown as SDKTransaction;
80
115
 
81
116
  const serialized = serializeTransaction(mockTransaction);
82
117
 
@@ -86,14 +121,14 @@ describe("logic utils", () => {
86
121
 
87
122
  it("should deserialize a hex string to a Transaction", () => {
88
123
  const mockTransaction = { id: "mock-transaction-id" };
89
- (Transaction.fromBytes as jest.Mock).mockReturnValue(mockTransaction);
124
+ (SDKTransaction.fromBytes as jest.Mock).mockReturnValue(mockTransaction);
90
125
 
91
126
  const hexTransaction = "0a141e2832";
92
127
  const deserialized = deserializeTransaction(hexTransaction);
93
128
 
94
129
  const hexTransactionBuffer = Buffer.from([10, 20, 30, 40, 50]);
95
- expect(Transaction.fromBytes).toHaveBeenCalledTimes(1);
96
- expect(Transaction.fromBytes).toHaveBeenCalledWith(hexTransactionBuffer);
130
+ expect(SDKTransaction.fromBytes).toHaveBeenCalledTimes(1);
131
+ expect(SDKTransaction.fromBytes).toHaveBeenCalledWith(hexTransactionBuffer);
97
132
  expect(deserialized).toBe(mockTransaction);
98
133
  });
99
134
  });
@@ -142,6 +177,75 @@ describe("logic utils", () => {
142
177
  });
143
178
  });
144
179
 
180
+ describe("mapIntentToSDKOperation", () => {
181
+ it("should return TokenAssociate for TokenAssociate intent", () => {
182
+ const txIntent = {
183
+ type: HEDERA_TRANSACTION_MODES.TokenAssociate,
184
+ } as TransactionIntent;
185
+
186
+ expect(mapIntentToSDKOperation(txIntent)).toBe(HEDERA_OPERATION_TYPES.TokenAssociate);
187
+ });
188
+
189
+ it("should return TokenTransfer for Send intent with HTS asset", () => {
190
+ const txIntent = {
191
+ type: HEDERA_TRANSACTION_MODES.Send,
192
+ asset: { type: "hts", assetReference: "0.0.1234" },
193
+ } as TransactionIntent;
194
+
195
+ expect(mapIntentToSDKOperation(txIntent)).toBe(HEDERA_OPERATION_TYPES.TokenTransfer);
196
+ });
197
+
198
+ it("should return ContractCall for Send intent with ERC20 asset", () => {
199
+ const txIntent = {
200
+ type: HEDERA_TRANSACTION_MODES.Send,
201
+ asset: { type: "erc20", assetReference: "0x1234" },
202
+ } as TransactionIntent;
203
+
204
+ expect(mapIntentToSDKOperation(txIntent)).toBe(HEDERA_OPERATION_TYPES.ContractCall);
205
+ });
206
+
207
+ it("should return CryptoUpdate for Delegate intent", () => {
208
+ const txIntent = {
209
+ type: HEDERA_TRANSACTION_MODES.Delegate,
210
+ } as TransactionIntent;
211
+
212
+ expect(mapIntentToSDKOperation(txIntent)).toBe(HEDERA_OPERATION_TYPES.CryptoUpdate);
213
+ });
214
+
215
+ it("should return CryptoUpdate for Undelegate intent", () => {
216
+ const txIntent = {
217
+ type: HEDERA_TRANSACTION_MODES.Undelegate,
218
+ } as TransactionIntent;
219
+
220
+ expect(mapIntentToSDKOperation(txIntent)).toBe(HEDERA_OPERATION_TYPES.CryptoUpdate);
221
+ });
222
+
223
+ it("should return CryptoUpdate for Redelegate intent", () => {
224
+ const txIntent = {
225
+ type: HEDERA_TRANSACTION_MODES.Redelegate,
226
+ } as TransactionIntent;
227
+
228
+ expect(mapIntentToSDKOperation(txIntent)).toBe(HEDERA_OPERATION_TYPES.CryptoUpdate);
229
+ });
230
+
231
+ it("should return CryptoTransfer for Send intent with native asset", () => {
232
+ const txIntent = {
233
+ type: HEDERA_TRANSACTION_MODES.Send,
234
+ asset: { type: "native" },
235
+ } as TransactionIntent;
236
+
237
+ expect(mapIntentToSDKOperation(txIntent)).toBe(HEDERA_OPERATION_TYPES.CryptoTransfer);
238
+ });
239
+
240
+ it("should return CryptoTransfer for other intent types", () => {
241
+ const txIntent = {
242
+ type: HEDERA_TRANSACTION_MODES.ClaimRewards,
243
+ } as TransactionIntent;
244
+
245
+ expect(mapIntentToSDKOperation(txIntent)).toBe(HEDERA_OPERATION_TYPES.CryptoTransfer);
246
+ });
247
+ });
248
+
145
249
  describe("getMemoFromBase64", () => {
146
250
  it("decodes a simple base64 string", () => {
147
251
  expect(getMemoFromBase64("YnJkZw==")).toBe("brdg");
@@ -247,6 +351,7 @@ describe("logic utils", () => {
247
351
  hederaResources: {
248
352
  maxAutomaticTokenAssociations: -1,
249
353
  isAutoTokenAssociationEnabled: true,
354
+ delegation: null,
250
355
  },
251
356
  });
252
357
 
@@ -391,6 +496,22 @@ describe("logic utils", () => {
391
496
  });
392
497
  });
393
498
 
499
+ describe("getChecksum", () => {
500
+ it("should return correct checksum for valid account ID", () => {
501
+ const accountId = "0.0.9124531-xrxlv";
502
+ const checksum = getChecksum(accountId);
503
+
504
+ expect(checksum).toBe("xrxlv");
505
+ });
506
+
507
+ it("should return null for invalid account ID", () => {
508
+ const accountId = "invalid-account-id";
509
+ const checksum = getChecksum(accountId);
510
+
511
+ expect(checksum).toBeNull();
512
+ });
513
+ });
514
+
394
515
  describe("safeParseAccountId", () => {
395
516
  it("returns account id and no checksum for valid address without checksum", () => {
396
517
  const [error, result] = safeParseAccountId("0.0.9124531");
@@ -597,4 +718,237 @@ describe("logic utils", () => {
597
718
  expect(result.end).toMatch(/^\d+\.000000000$/);
598
719
  });
599
720
  });
721
+
722
+ describe("isStakingTransaction", () => {
723
+ it("returns correct value based on tx.mode", () => {
724
+ const stakingDelegateTx = { mode: HEDERA_TRANSACTION_MODES.Delegate } as Transaction;
725
+ const stakingUndelegateTx = { mode: HEDERA_TRANSACTION_MODES.Undelegate } as Transaction;
726
+ const stakingRedelegateTx = { mode: HEDERA_TRANSACTION_MODES.Redelegate } as Transaction;
727
+ const stakingClaimRewardsTx = { mode: HEDERA_TRANSACTION_MODES.ClaimRewards } as Transaction;
728
+ const transferTx = { recipient: "", amount: new BigNumber(1) } as Transaction;
729
+ const emptyTx = {} as Transaction;
730
+
731
+ expect(isStakingTransaction(stakingDelegateTx)).toBe(true);
732
+ expect(isStakingTransaction(stakingUndelegateTx)).toBe(true);
733
+ expect(isStakingTransaction(stakingRedelegateTx)).toBe(true);
734
+ expect(isStakingTransaction(stakingClaimRewardsTx)).toBe(true);
735
+ expect(isStakingTransaction(transferTx)).toBe(false);
736
+ expect(isStakingTransaction(emptyTx)).toBe(false);
737
+ });
738
+
739
+ it("returns false for undefined or null transactions", () => {
740
+ expect(isStakingTransaction(undefined as unknown as Transaction)).toBe(false);
741
+ expect(isStakingTransaction(null as unknown as Transaction)).toBe(false);
742
+ });
743
+ });
744
+
745
+ describe("extractCompanyFromNodeDescription", () => {
746
+ it("extracts company name from description", () => {
747
+ expect(extractCompanyFromNodeDescription("Hosted by Ledger | Paris, France")).toBe("Ledger");
748
+ expect(extractCompanyFromNodeDescription("Hosted by LG | Seoul, South Korea")).toBe("LG");
749
+ expect(extractCompanyFromNodeDescription("TestCompany | something else")).toBe("TestCompany");
750
+ expect(extractCompanyFromNodeDescription("NoSeparator ")).toBe("NoSeparator");
751
+ });
752
+ });
753
+
754
+ describe("sortValidators", () => {
755
+ it("sorts validators by active stake DESC, Ledger node first if set", () => {
756
+ setEnv("HEDERA_STAKING_LEDGER_NODE_ID", 2);
757
+
758
+ const validators = [
759
+ { nodeId: 3, activeStake: new BigNumber(1000) },
760
+ { nodeId: 2, activeStake: new BigNumber(2000) },
761
+ { nodeId: 1, activeStake: new BigNumber(3000) },
762
+ ] as HederaValidator[];
763
+
764
+ const sorted = sortValidators(validators);
765
+
766
+ expect(sorted[0].nodeId).toBe(2);
767
+ expect(sorted[1].nodeId).toBe(1);
768
+ expect(sorted[2].nodeId).toBe(3);
769
+ });
770
+ });
771
+
772
+ describe("getValidatorFromAccount", () => {
773
+ const mockValidator = { nodeId: 1 };
774
+ const mockPreload = { validators: [mockValidator] } as HederaPreloadData;
775
+
776
+ beforeEach(() => {
777
+ jest.clearAllMocks();
778
+
779
+ jest.spyOn(preloadData, "getCurrentHederaPreloadData").mockReturnValueOnce(mockPreload);
780
+ });
781
+
782
+ it("returns validator matching delegation nodeId", () => {
783
+ const mockAccount = {
784
+ currency: "hedera",
785
+ hederaResources: { delegation: { nodeId: 1 } },
786
+ } as unknown as HederaAccount;
787
+
788
+ expect(getValidatorFromAccount(mockAccount)).toEqual(mockValidator);
789
+ });
790
+
791
+ it("returns null if no delegation", () => {
792
+ const mockAccount = {
793
+ currency: "hedera",
794
+ hederaResources: {},
795
+ } as unknown as HederaAccount;
796
+
797
+ expect(getValidatorFromAccount(mockAccount)).toBeNull();
798
+ });
799
+ });
800
+
801
+ describe("getDefaultValidator", () => {
802
+ const mockValidators = [
803
+ { nodeId: 1, activeStake: new BigNumber(2000) },
804
+ { nodeId: 2, activeStake: new BigNumber(1000) },
805
+ { nodeId: 3, activeStake: new BigNumber(10000) },
806
+ ] as HederaValidator[];
807
+
808
+ it("returns Ledger validator if present", () => {
809
+ setEnv("HEDERA_STAKING_LEDGER_NODE_ID", 2);
810
+ expect(getDefaultValidator(mockValidators)?.nodeId).toBe(2);
811
+ });
812
+
813
+ it("returns null if no Ledger validator is present", () => {
814
+ expect(getDefaultValidator([])).toBeNull();
815
+ });
816
+ });
817
+
818
+ describe("getDelegationStatus", () => {
819
+ const mockValidator = { address: "0.0.3", overstaked: false } as HederaValidator;
820
+ const mockOverstakedValidator = { address: "0.0.3", overstaked: true } as HederaValidator;
821
+
822
+ it("returns inactive if validator or validator's address is missing", () => {
823
+ expect(getDelegationStatus(null)).toBe("inactive");
824
+ expect(getDelegationStatus({ address: "" } as any)).toBe("inactive");
825
+ });
826
+
827
+ it("returns overstaked if validator.overstaked is true", () => {
828
+ expect(getDelegationStatus(mockOverstakedValidator)).toBe("overstaked");
829
+ });
830
+
831
+ it("returns active otherwise", () => {
832
+ expect(getDelegationStatus(mockValidator)).toBe("active");
833
+ });
834
+ });
835
+
836
+ describe("filterValidatorBySearchTerm", () => {
837
+ const mockValidator: HederaValidator = {
838
+ nodeId: 123,
839
+ name: "Validator Test",
840
+ address: "0.0.456",
841
+ addressChecksum: "abcde",
842
+ minStake: new BigNumber(0),
843
+ maxStake: new BigNumber(0),
844
+ activeStake: new BigNumber(0),
845
+ activeStakePercentage: new BigNumber(0),
846
+ overstaked: false,
847
+ };
848
+
849
+ it("should match by nodeId", () => {
850
+ expect(filterValidatorBySearchTerm(mockValidator, "123")).toBe(true);
851
+ });
852
+
853
+ it("should match by name with case insensitivity", () => {
854
+ expect(filterValidatorBySearchTerm(mockValidator, "validator")).toBe(true);
855
+ expect(filterValidatorBySearchTerm(mockValidator, "VALIDATOR")).toBe(true);
856
+ expect(filterValidatorBySearchTerm(mockValidator, "test")).toBe(true);
857
+ expect(filterValidatorBySearchTerm(mockValidator, "unknown")).toBe(false);
858
+ });
859
+
860
+ it("should match by address", () => {
861
+ expect(filterValidatorBySearchTerm(mockValidator, "0.0.456")).toBe(true);
862
+ expect(filterValidatorBySearchTerm(mockValidator, "456")).toBe(true);
863
+ expect(filterValidatorBySearchTerm(mockValidator, "789")).toBe(false);
864
+ });
865
+
866
+ it("should match by address with checksum", () => {
867
+ expect(filterValidatorBySearchTerm(mockValidator, "0.0.456-abcde")).toBe(true);
868
+ expect(filterValidatorBySearchTerm(mockValidator, "abcde")).toBe(true);
869
+ expect(filterValidatorBySearchTerm(mockValidator, "ABC")).toBe(true);
870
+ });
871
+
872
+ it("should handle validator without checksum", () => {
873
+ const validatorWithoutChecksum = { ...mockValidator, addressChecksum: null };
874
+ expect(filterValidatorBySearchTerm(validatorWithoutChecksum, "0.0.456")).toBe(true);
875
+ expect(filterValidatorBySearchTerm(validatorWithoutChecksum, "abcde")).toBe(false);
876
+ });
877
+
878
+ it("should handle empty search term", () => {
879
+ expect(filterValidatorBySearchTerm(mockValidator, "")).toBe(true);
880
+ });
881
+
882
+ it("should handle partial matches", () => {
883
+ expect(filterValidatorBySearchTerm(mockValidator, "valid")).toBe(true);
884
+ expect(filterValidatorBySearchTerm(mockValidator, "0.0")).toBe(true);
885
+ expect(filterValidatorBySearchTerm(mockValidator, "12")).toBe(true);
886
+ });
887
+ });
888
+
889
+ describe("hasSpecificIntentData", () => {
890
+ it("should return true when txIntent has data matching expected type", () => {
891
+ const stakingTxIntent = {
892
+ data: { type: "staking" as const },
893
+ } as TransactionIntent<HederaMemo, HederaTxData>;
894
+ const erc20TxIntent = {
895
+ data: { type: "erc20" as const },
896
+ } as TransactionIntent<HederaMemo, HederaTxData>;
897
+
898
+ expect(hasSpecificIntentData(stakingTxIntent, "staking")).toBe(true);
899
+ expect(hasSpecificIntentData(erc20TxIntent, "erc20")).toBe(true);
900
+ });
901
+
902
+ it("should return false when txIntent has invalid data", () => {
903
+ const txIntentNoData = {} as TransactionIntent<HederaMemo, HederaTxData>;
904
+ const txIntentUnknown = {
905
+ data: { type: "unknown" as const },
906
+ } as unknown as TransactionIntent<HederaMemo, HederaTxData>;
907
+
908
+ expect(hasSpecificIntentData(txIntentUnknown, "erc20")).toBe(false);
909
+ expect(hasSpecificIntentData(txIntentNoData, "erc20")).toBe(false);
910
+ });
911
+ });
912
+
913
+ describe("getOperationDetailsExtraFields", () => {
914
+ it("should return empty array when no fields are present", () => {
915
+ const result = getOperationDetailsExtraFields({});
916
+
917
+ expect(result).toEqual([]);
918
+ });
919
+
920
+ it("should handle zero values correctly", () => {
921
+ const result = getOperationDetailsExtraFields({
922
+ gasConsumed: 0,
923
+ targetStakingNodeId: 0,
924
+ });
925
+
926
+ expect(result).toEqual([
927
+ { key: "targetStakingNodeId", value: "0" },
928
+ { key: "gasConsumed", value: "0" },
929
+ ]);
930
+ });
931
+
932
+ it("should return all fields when all are present", () => {
933
+ const result = getOperationDetailsExtraFields({
934
+ memo: "complete",
935
+ associatedTokenId: "123",
936
+ targetStakingNodeId: 5,
937
+ previousStakingNodeId: 3,
938
+ gasConsumed: 1000,
939
+ gasUsed: 950,
940
+ gasLimit: 2000,
941
+ });
942
+
943
+ expect(result).toEqual([
944
+ { key: "memo", value: "complete" },
945
+ { key: "associatedTokenId", value: "123" },
946
+ { key: "targetStakingNodeId", value: "5" },
947
+ { key: "previousStakingNodeId", value: "3" },
948
+ { key: "gasConsumed", value: "1000" },
949
+ { key: "gasUsed", value: "950" },
950
+ { key: "gasLimit", value: "2000" },
951
+ ]);
952
+ });
953
+ });
600
954
  });
@@ -12,10 +12,12 @@ import { findCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies";
12
12
  import { getFiatCurrencyByTicker } from "@ledgerhq/cryptoassets/fiats";
13
13
  import cvsApi from "@ledgerhq/live-countervalues/api/index";
14
14
  import { InvalidAddress } from "@ledgerhq/errors";
15
+ import { getEnv } from "@ledgerhq/live-env";
15
16
  import { makeLRUCache, seconds } from "@ledgerhq/live-network/cache";
16
17
  import type { Currency, ExplorerView, TokenCurrency } from "@ledgerhq/types-cryptoassets";
17
18
  import type { AccountLike, Operation as LiveOperation } from "@ledgerhq/types-live";
18
19
  import {
20
+ HEDERA_DELEGATION_STATUS,
19
21
  HEDERA_OPERATION_TYPES,
20
22
  HEDERA_TRANSACTION_MODES,
21
23
  SYNTHETIC_BLOCK_WINDOW_SECONDS,
@@ -23,13 +25,19 @@ import {
23
25
  import { apiClient } from "../network/api";
24
26
  import type {
25
27
  HederaAccount,
28
+ HederaMemo,
26
29
  HederaOperationExtra,
30
+ HederaTxData,
31
+ HederaValidator,
32
+ OperationDetailsExtraField,
27
33
  Transaction,
34
+ TransactionStaking,
28
35
  TransactionStatus,
29
36
  TransactionTokenAssociate,
30
37
  } from "../types";
31
38
  import { rpcClient } from "../network/rpc";
32
39
  import { HederaRecipientInvalidChecksum } from "../errors";
40
+ import { getCurrentHederaPreloadData } from "../preload-data";
33
41
 
34
42
  export const serializeSignature = (signature: Uint8Array) => {
35
43
  return Buffer.from(signature).toString("base64");
@@ -88,6 +96,14 @@ export const mapIntentToSDKOperation = (txIntent: TransactionIntent) => {
88
96
  return HEDERA_OPERATION_TYPES.ContractCall;
89
97
  }
90
98
 
99
+ if (
100
+ txIntent.type === HEDERA_TRANSACTION_MODES.Delegate ||
101
+ txIntent.type === HEDERA_TRANSACTION_MODES.Undelegate ||
102
+ txIntent.type === HEDERA_TRANSACTION_MODES.Redelegate
103
+ ) {
104
+ return HEDERA_OPERATION_TYPES.CryptoUpdate;
105
+ }
106
+
91
107
  return HEDERA_OPERATION_TYPES.CryptoTransfer;
92
108
  };
93
109
 
@@ -122,8 +138,10 @@ export const getTransactionExplorer = (
122
138
  );
123
139
  };
124
140
 
125
- export const isTokenAssociateTransaction = (tx: Transaction): tx is TransactionTokenAssociate => {
126
- return tx.mode === HEDERA_TRANSACTION_MODES.TokenAssociate;
141
+ export const isTokenAssociateTransaction = (
142
+ tx: Transaction | null | undefined,
143
+ ): tx is TransactionTokenAssociate => {
144
+ return tx?.mode === HEDERA_TRANSACTION_MODES.TokenAssociate;
127
145
  };
128
146
 
129
147
  export const isAutoTokenAssociationEnabled = (account: AccountLike) => {
@@ -150,6 +168,42 @@ export const isValidExtra = (extra: unknown): extra is HederaOperationExtra => {
150
168
  return !!extra && typeof extra === "object" && !Array.isArray(extra);
151
169
  };
152
170
 
171
+ export const getOperationDetailsExtraFields = (
172
+ extra: HederaOperationExtra,
173
+ ): OperationDetailsExtraField[] => {
174
+ const fields: OperationDetailsExtraField[] = [];
175
+
176
+ if (typeof extra.memo === "string") {
177
+ fields.push({ key: "memo", value: extra.memo });
178
+ }
179
+
180
+ if (typeof extra.associatedTokenId === "string") {
181
+ fields.push({ key: "associatedTokenId", value: extra.associatedTokenId });
182
+ }
183
+
184
+ if (typeof extra.targetStakingNodeId === "number") {
185
+ fields.push({ key: "targetStakingNodeId", value: extra.targetStakingNodeId.toString() });
186
+ }
187
+
188
+ if (typeof extra.previousStakingNodeId === "number") {
189
+ fields.push({ key: "previousStakingNodeId", value: extra.previousStakingNodeId.toString() });
190
+ }
191
+
192
+ if (typeof extra.gasConsumed === "number") {
193
+ fields.push({ key: "gasConsumed", value: extra.gasConsumed.toString() });
194
+ }
195
+
196
+ if (typeof extra.gasUsed === "number") {
197
+ fields.push({ key: "gasUsed", value: extra.gasUsed.toString() });
198
+ }
199
+
200
+ if (typeof extra.gasLimit === "number") {
201
+ fields.push({ key: "gasLimit", value: extra.gasLimit.toString() });
202
+ }
203
+
204
+ return fields;
205
+ };
206
+
153
207
  // disables the "Continue" button in the Send modal's Recipient step during token transfers if:
154
208
  // - the recipient is not associated with the token
155
209
  // - the association status can't be verified
@@ -212,6 +266,15 @@ export const checkAccountTokenAssociationStatus = makeLRUCache(
212
266
  seconds(30),
213
267
  );
214
268
 
269
+ export const getChecksum = (accountId: string): string | null => {
270
+ try {
271
+ const entityId = EntityIdHelper.fromString(accountId);
272
+ return entityId.checksum ?? null;
273
+ } catch {
274
+ return null;
275
+ }
276
+ };
277
+
215
278
  export const safeParseAccountId = (
216
279
  address: string,
217
280
  ): [Error, null] | [null, { accountId: string; checksum: string | null }] => {
@@ -220,7 +283,7 @@ export const safeParseAccountId = (
220
283
 
221
284
  try {
222
285
  const accountId = AccountId.fromString(address);
223
- const checksum = EntityIdHelper.fromString(address).checksum ?? null;
286
+ const checksum = getChecksum(address);
224
287
 
225
288
  if (checksum) {
226
289
  const client = rpcClient.getInstance();
@@ -237,7 +300,7 @@ export const safeParseAccountId = (
237
300
  };
238
301
 
239
302
  return [null, result];
240
- } catch (err) {
303
+ } catch {
241
304
  return [new InvalidAddress("", { currencyName }), null];
242
305
  }
243
306
  };
@@ -339,3 +402,94 @@ export const fromEVMAddress = (evmAddress: string, shard = 0, realm = 0): string
339
402
  return null;
340
403
  }
341
404
  };
405
+
406
+ export const extractCompanyFromNodeDescription = (description: string): string => {
407
+ return description
408
+ .split("|")[0]
409
+ .replace(/hosted by/i, "")
410
+ .replace(/hosted for/i, "")
411
+ .trim();
412
+ };
413
+
414
+ export const sortValidators = (validators: HederaValidator[]): HederaValidator[] => {
415
+ const ledgerNodeId = getEnv("HEDERA_STAKING_LEDGER_NODE_ID");
416
+
417
+ // sort validators by active stake in DESC order, with Ledger node first if it exists
418
+ return [...validators].sort((a, b) => {
419
+ if (typeof ledgerNodeId === "number") {
420
+ if (a.nodeId === ledgerNodeId) return -1;
421
+ if (b.nodeId === ledgerNodeId) return 1;
422
+ }
423
+
424
+ return b.activeStake.toNumber() - a.activeStake.toNumber();
425
+ });
426
+ };
427
+
428
+ export const filterValidatorBySearchTerm = (
429
+ validator: HederaValidator,
430
+ search: string,
431
+ ): boolean => {
432
+ const lowercaseSearch = search.toLowerCase();
433
+ const addressWithChecksum = validator.addressChecksum
434
+ ? `${validator.address}-${validator.addressChecksum}`
435
+ : validator.address;
436
+
437
+ return (
438
+ validator.nodeId.toString().includes(lowercaseSearch) ||
439
+ validator.name.toLowerCase().includes(lowercaseSearch) ||
440
+ addressWithChecksum.toLowerCase().includes(lowercaseSearch)
441
+ );
442
+ };
443
+
444
+ export const getValidatorFromAccount = (account: HederaAccount): HederaValidator | null => {
445
+ const { delegation } = account.hederaResources ?? {};
446
+
447
+ if (!delegation) {
448
+ return null;
449
+ }
450
+
451
+ const validators = getCurrentHederaPreloadData(account.currency);
452
+ const validator = validators.validators.find(v => v.nodeId === delegation.nodeId) ?? null;
453
+
454
+ return validator;
455
+ };
456
+
457
+ export const getDefaultValidator = (validators: HederaValidator[]): HederaValidator | null => {
458
+ const ledgerNodeId = getEnv("HEDERA_STAKING_LEDGER_NODE_ID");
459
+
460
+ return validators.find(v => v.nodeId === ledgerNodeId) ?? null;
461
+ };
462
+
463
+ export const getDelegationStatus = (
464
+ validator: HederaValidator | null,
465
+ ): HEDERA_DELEGATION_STATUS => {
466
+ if (!validator?.address) {
467
+ return HEDERA_DELEGATION_STATUS.Inactive;
468
+ }
469
+
470
+ if (validator.overstaked) {
471
+ return HEDERA_DELEGATION_STATUS.Overstaked;
472
+ }
473
+
474
+ return HEDERA_DELEGATION_STATUS.Active;
475
+ };
476
+
477
+ export const isStakingTransaction = (
478
+ tx: Transaction | null | undefined,
479
+ ): tx is TransactionStaking => {
480
+ if (!tx) return false;
481
+
482
+ return (
483
+ tx.mode === HEDERA_TRANSACTION_MODES.Delegate ||
484
+ tx.mode === HEDERA_TRANSACTION_MODES.Undelegate ||
485
+ tx.mode === HEDERA_TRANSACTION_MODES.Redelegate ||
486
+ tx.mode === HEDERA_TRANSACTION_MODES.ClaimRewards
487
+ );
488
+ };
489
+
490
+ export const hasSpecificIntentData = <Type extends "staking" | "erc20">(
491
+ txIntent: TransactionIntent<HederaMemo, HederaTxData>,
492
+ expectedType: Type,
493
+ ): txIntent is Extract<TransactionIntent<HederaMemo, HederaTxData>, { data: { type: Type } }> => {
494
+ return "data" in txIntent && txIntent.data.type === expectedType;
495
+ };