@ledgerhq/coin-hedera 1.10.1 → 1.10.2-nightly.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/.eslintrc.js +1 -0
  2. package/CHANGELOG.md +10 -0
  3. package/lib/api/mirror.d.ts +3 -20
  4. package/lib/api/mirror.d.ts.map +1 -1
  5. package/lib/api/mirror.js +32 -90
  6. package/lib/api/mirror.js.map +1 -1
  7. package/lib/api/mirror.test.js +59 -4
  8. package/lib/api/mirror.test.js.map +1 -1
  9. package/lib/api/network.d.ts +3 -3
  10. package/lib/api/network.d.ts.map +1 -1
  11. package/lib/api/network.js +46 -3
  12. package/lib/api/network.js.map +1 -1
  13. package/lib/api/types.d.ts +44 -0
  14. package/lib/api/types.d.ts.map +1 -0
  15. package/lib/api/types.js +3 -0
  16. package/lib/api/types.js.map +1 -0
  17. package/lib/api/utils.d.ts +8 -0
  18. package/lib/api/utils.d.ts.map +1 -0
  19. package/lib/api/utils.js +132 -0
  20. package/lib/api/utils.js.map +1 -0
  21. package/lib/bridge/broadcast.d.ts.map +1 -1
  22. package/lib/bridge/broadcast.js +2 -0
  23. package/lib/bridge/broadcast.js.map +1 -1
  24. package/lib/bridge/buildOptimisticOperation.d.ts +2 -2
  25. package/lib/bridge/buildOptimisticOperation.d.ts.map +1 -1
  26. package/lib/bridge/buildOptimisticOperation.integration.test.d.ts +2 -0
  27. package/lib/bridge/buildOptimisticOperation.integration.test.d.ts.map +1 -0
  28. package/lib/bridge/buildOptimisticOperation.integration.test.js +82 -0
  29. package/lib/bridge/buildOptimisticOperation.integration.test.js.map +1 -0
  30. package/lib/bridge/buildOptimisticOperation.js +87 -5
  31. package/lib/bridge/buildOptimisticOperation.js.map +1 -1
  32. package/lib/bridge/estimateMaxSpendable.d.ts.map +1 -1
  33. package/lib/bridge/estimateMaxSpendable.js +8 -2
  34. package/lib/bridge/estimateMaxSpendable.js.map +1 -1
  35. package/lib/bridge/getTransactionStatus.d.ts +3 -3
  36. package/lib/bridge/getTransactionStatus.d.ts.map +1 -1
  37. package/lib/bridge/getTransactionStatus.js +116 -23
  38. package/lib/bridge/getTransactionStatus.js.map +1 -1
  39. package/lib/bridge/getTransactionStatus.test.d.ts +2 -0
  40. package/lib/bridge/getTransactionStatus.test.d.ts.map +1 -0
  41. package/lib/bridge/getTransactionStatus.test.js +176 -0
  42. package/lib/bridge/getTransactionStatus.test.js.map +1 -0
  43. package/lib/bridge/index.d.ts +4 -4
  44. package/lib/bridge/index.d.ts.map +1 -1
  45. package/lib/bridge/index.js +9 -6
  46. package/lib/bridge/index.js.map +1 -1
  47. package/lib/bridge/js-estimateMaxSpendable.integration.test.js +28 -44
  48. package/lib/bridge/js-estimateMaxSpendable.integration.test.js.map +1 -1
  49. package/lib/bridge/js-transaction.test.js +10 -49
  50. package/lib/bridge/js-transaction.test.js.map +1 -1
  51. package/lib/bridge/prepareTransaction.d.ts +0 -1
  52. package/lib/bridge/prepareTransaction.d.ts.map +1 -1
  53. package/lib/bridge/prepareTransaction.js +0 -1
  54. package/lib/bridge/prepareTransaction.js.map +1 -1
  55. package/lib/bridge/serialization.d.ts +7 -0
  56. package/lib/bridge/serialization.d.ts.map +1 -0
  57. package/lib/bridge/serialization.js +36 -0
  58. package/lib/bridge/serialization.js.map +1 -0
  59. package/lib/bridge/serialization.test.d.ts +2 -0
  60. package/lib/bridge/serialization.test.d.ts.map +1 -0
  61. package/lib/bridge/serialization.test.js +27 -0
  62. package/lib/bridge/serialization.test.js.map +1 -0
  63. package/lib/bridge/synchronisation.d.ts +3 -3
  64. package/lib/bridge/synchronisation.d.ts.map +1 -1
  65. package/lib/bridge/synchronisation.js +37 -15
  66. package/lib/bridge/synchronisation.js.map +1 -1
  67. package/lib/bridge/transaction.test.js +18 -59
  68. package/lib/bridge/transaction.test.js.map +1 -1
  69. package/lib/bridge/utils.d.ts +22 -8
  70. package/lib/bridge/utils.d.ts.map +1 -1
  71. package/lib/bridge/utils.integration.test.js +415 -73
  72. package/lib/bridge/utils.integration.test.js.map +1 -1
  73. package/lib/bridge/utils.js +300 -15
  74. package/lib/bridge/utils.js.map +1 -1
  75. package/lib/constants.d.ts +32 -0
  76. package/lib/constants.d.ts.map +1 -0
  77. package/lib/constants.js +37 -0
  78. package/lib/constants.js.map +1 -0
  79. package/lib/deviceTransactionConfig.d.ts.map +1 -1
  80. package/lib/deviceTransactionConfig.js +17 -15
  81. package/lib/deviceTransactionConfig.js.map +1 -1
  82. package/lib/logic.d.ts +9 -3
  83. package/lib/logic.d.ts.map +1 -1
  84. package/lib/logic.js +31 -3
  85. package/lib/logic.js.map +1 -1
  86. package/lib/logic.test.js +103 -50
  87. package/lib/logic.test.js.map +1 -1
  88. package/lib/test/fixtures/account.fixture.d.ts +19 -0
  89. package/lib/test/fixtures/account.fixture.d.ts.map +1 -0
  90. package/lib/test/fixtures/account.fixture.js +116 -0
  91. package/lib/test/fixtures/account.fixture.js.map +1 -0
  92. package/lib/test/fixtures/currency.fixture.d.ts +5 -0
  93. package/lib/test/fixtures/currency.fixture.d.ts.map +1 -0
  94. package/lib/test/fixtures/currency.fixture.js +67 -0
  95. package/lib/test/fixtures/currency.fixture.js.map +1 -0
  96. package/lib/test/fixtures/mirror.fixture.d.ts +3 -0
  97. package/lib/test/fixtures/mirror.fixture.d.ts.map +1 -0
  98. package/lib/test/fixtures/mirror.fixture.js +17 -0
  99. package/lib/test/fixtures/mirror.fixture.js.map +1 -0
  100. package/lib/test/fixtures/operation.fixture.d.ts +3 -0
  101. package/lib/test/fixtures/operation.fixture.d.ts.map +1 -0
  102. package/lib/test/fixtures/operation.fixture.js +26 -0
  103. package/lib/test/fixtures/operation.fixture.js.map +1 -0
  104. package/lib/test/fixtures/transaction.fixture.d.ts +4 -0
  105. package/lib/test/fixtures/transaction.fixture.d.ts.map +1 -0
  106. package/lib/test/fixtures/transaction.fixture.js +28 -0
  107. package/lib/test/fixtures/transaction.fixture.js.map +1 -0
  108. package/lib/types/bridge.d.ts +25 -1
  109. package/lib/types/bridge.d.ts.map +1 -1
  110. package/lib-es/api/mirror.d.ts +3 -20
  111. package/lib-es/api/mirror.d.ts.map +1 -1
  112. package/lib-es/api/mirror.js +29 -88
  113. package/lib-es/api/mirror.js.map +1 -1
  114. package/lib-es/api/mirror.test.js +60 -5
  115. package/lib-es/api/mirror.test.js.map +1 -1
  116. package/lib-es/api/network.d.ts +3 -3
  117. package/lib-es/api/network.d.ts.map +1 -1
  118. package/lib-es/api/network.js +44 -4
  119. package/lib-es/api/network.js.map +1 -1
  120. package/lib-es/api/types.d.ts +44 -0
  121. package/lib-es/api/types.d.ts.map +1 -0
  122. package/lib-es/api/types.js +2 -0
  123. package/lib-es/api/types.js.map +1 -0
  124. package/lib-es/api/utils.d.ts +8 -0
  125. package/lib-es/api/utils.d.ts.map +1 -0
  126. package/lib-es/api/utils.js +124 -0
  127. package/lib-es/api/utils.js.map +1 -0
  128. package/lib-es/bridge/broadcast.d.ts.map +1 -1
  129. package/lib-es/bridge/broadcast.js +2 -0
  130. package/lib-es/bridge/broadcast.js.map +1 -1
  131. package/lib-es/bridge/buildOptimisticOperation.d.ts +2 -2
  132. package/lib-es/bridge/buildOptimisticOperation.d.ts.map +1 -1
  133. package/lib-es/bridge/buildOptimisticOperation.integration.test.d.ts +2 -0
  134. package/lib-es/bridge/buildOptimisticOperation.integration.test.d.ts.map +1 -0
  135. package/lib-es/bridge/buildOptimisticOperation.integration.test.js +77 -0
  136. package/lib-es/bridge/buildOptimisticOperation.integration.test.js.map +1 -0
  137. package/lib-es/bridge/buildOptimisticOperation.js +84 -5
  138. package/lib-es/bridge/buildOptimisticOperation.js.map +1 -1
  139. package/lib-es/bridge/estimateMaxSpendable.d.ts.map +1 -1
  140. package/lib-es/bridge/estimateMaxSpendable.js +8 -2
  141. package/lib-es/bridge/estimateMaxSpendable.js.map +1 -1
  142. package/lib-es/bridge/getTransactionStatus.d.ts +3 -3
  143. package/lib-es/bridge/getTransactionStatus.d.ts.map +1 -1
  144. package/lib-es/bridge/getTransactionStatus.js +114 -24
  145. package/lib-es/bridge/getTransactionStatus.js.map +1 -1
  146. package/lib-es/bridge/getTransactionStatus.test.d.ts +2 -0
  147. package/lib-es/bridge/getTransactionStatus.test.d.ts.map +1 -0
  148. package/lib-es/bridge/getTransactionStatus.test.js +148 -0
  149. package/lib-es/bridge/getTransactionStatus.test.js.map +1 -0
  150. package/lib-es/bridge/index.d.ts +4 -4
  151. package/lib-es/bridge/index.d.ts.map +1 -1
  152. package/lib-es/bridge/index.js +9 -6
  153. package/lib-es/bridge/index.js.map +1 -1
  154. package/lib-es/bridge/js-estimateMaxSpendable.integration.test.js +28 -44
  155. package/lib-es/bridge/js-estimateMaxSpendable.integration.test.js.map +1 -1
  156. package/lib-es/bridge/js-transaction.test.js +10 -49
  157. package/lib-es/bridge/js-transaction.test.js.map +1 -1
  158. package/lib-es/bridge/prepareTransaction.d.ts +0 -1
  159. package/lib-es/bridge/prepareTransaction.d.ts.map +1 -1
  160. package/lib-es/bridge/prepareTransaction.js +0 -1
  161. package/lib-es/bridge/prepareTransaction.js.map +1 -1
  162. package/lib-es/bridge/serialization.d.ts +7 -0
  163. package/lib-es/bridge/serialization.d.ts.map +1 -0
  164. package/lib-es/bridge/serialization.js +29 -0
  165. package/lib-es/bridge/serialization.js.map +1 -0
  166. package/lib-es/bridge/serialization.test.d.ts +2 -0
  167. package/lib-es/bridge/serialization.test.d.ts.map +1 -0
  168. package/lib-es/bridge/serialization.test.js +25 -0
  169. package/lib-es/bridge/serialization.test.js.map +1 -0
  170. package/lib-es/bridge/synchronisation.d.ts +3 -3
  171. package/lib-es/bridge/synchronisation.d.ts.map +1 -1
  172. package/lib-es/bridge/synchronisation.js +39 -17
  173. package/lib-es/bridge/synchronisation.js.map +1 -1
  174. package/lib-es/bridge/transaction.test.js +18 -59
  175. package/lib-es/bridge/transaction.test.js.map +1 -1
  176. package/lib-es/bridge/utils.d.ts +22 -8
  177. package/lib-es/bridge/utils.d.ts.map +1 -1
  178. package/lib-es/bridge/utils.integration.test.js +416 -74
  179. package/lib-es/bridge/utils.integration.test.js.map +1 -1
  180. package/lib-es/bridge/utils.js +295 -15
  181. package/lib-es/bridge/utils.js.map +1 -1
  182. package/lib-es/constants.d.ts +32 -0
  183. package/lib-es/constants.d.ts.map +1 -0
  184. package/lib-es/constants.js +34 -0
  185. package/lib-es/constants.js.map +1 -0
  186. package/lib-es/deviceTransactionConfig.d.ts.map +1 -1
  187. package/lib-es/deviceTransactionConfig.js +17 -15
  188. package/lib-es/deviceTransactionConfig.js.map +1 -1
  189. package/lib-es/logic.d.ts +9 -3
  190. package/lib-es/logic.d.ts.map +1 -1
  191. package/lib-es/logic.js +26 -3
  192. package/lib-es/logic.js.map +1 -1
  193. package/lib-es/logic.test.js +104 -51
  194. package/lib-es/logic.test.js.map +1 -1
  195. package/lib-es/test/fixtures/account.fixture.d.ts +19 -0
  196. package/lib-es/test/fixtures/account.fixture.d.ts.map +1 -0
  197. package/lib-es/test/fixtures/account.fixture.js +107 -0
  198. package/lib-es/test/fixtures/account.fixture.js.map +1 -0
  199. package/lib-es/test/fixtures/currency.fixture.d.ts +5 -0
  200. package/lib-es/test/fixtures/currency.fixture.d.ts.map +1 -0
  201. package/lib-es/test/fixtures/currency.fixture.js +58 -0
  202. package/lib-es/test/fixtures/currency.fixture.js.map +1 -0
  203. package/lib-es/test/fixtures/mirror.fixture.d.ts +3 -0
  204. package/lib-es/test/fixtures/mirror.fixture.d.ts.map +1 -0
  205. package/lib-es/test/fixtures/mirror.fixture.js +13 -0
  206. package/lib-es/test/fixtures/mirror.fixture.js.map +1 -0
  207. package/lib-es/test/fixtures/operation.fixture.d.ts +3 -0
  208. package/lib-es/test/fixtures/operation.fixture.d.ts.map +1 -0
  209. package/lib-es/test/fixtures/operation.fixture.js +19 -0
  210. package/lib-es/test/fixtures/operation.fixture.js.map +1 -0
  211. package/lib-es/test/fixtures/transaction.fixture.d.ts +4 -0
  212. package/lib-es/test/fixtures/transaction.fixture.d.ts.map +1 -0
  213. package/lib-es/test/fixtures/transaction.fixture.js +20 -0
  214. package/lib-es/test/fixtures/transaction.fixture.js.map +1 -0
  215. package/lib-es/types/bridge.d.ts +25 -1
  216. package/lib-es/types/bridge.d.ts.map +1 -1
  217. package/package.json +11 -9
  218. package/src/api/mirror.test.ts +79 -5
  219. package/src/api/mirror.ts +30 -111
  220. package/src/api/network.ts +71 -4
  221. package/src/api/types.ts +48 -0
  222. package/src/api/utils.ts +150 -0
  223. package/src/bridge/broadcast.ts +2 -0
  224. package/src/bridge/buildOptimisticOperation.integration.test.ts +88 -0
  225. package/src/bridge/buildOptimisticOperation.ts +118 -7
  226. package/src/bridge/estimateMaxSpendable.ts +8 -2
  227. package/src/bridge/getTransactionStatus.test.ts +200 -0
  228. package/src/bridge/getTransactionStatus.ts +166 -32
  229. package/src/bridge/index.ts +13 -10
  230. package/src/bridge/js-estimateMaxSpendable.integration.test.ts +37 -46
  231. package/src/bridge/js-transaction.test.ts +13 -54
  232. package/src/bridge/prepareTransaction.ts +1 -2
  233. package/src/bridge/serialization.test.ts +39 -0
  234. package/src/bridge/serialization.ts +43 -0
  235. package/src/bridge/synchronisation.ts +65 -27
  236. package/src/bridge/transaction.test.ts +22 -64
  237. package/src/bridge/utils.integration.test.ts +525 -76
  238. package/src/bridge/utils.ts +423 -24
  239. package/src/constants.ts +35 -0
  240. package/src/deviceTransactionConfig.ts +16 -15
  241. package/src/logic.test.ts +147 -57
  242. package/src/logic.ts +58 -7
  243. package/src/test/fixtures/account.fixture.ts +123 -0
  244. package/src/test/fixtures/currency.fixture.ts +66 -0
  245. package/src/test/fixtures/mirror.fixture.ts +14 -0
  246. package/src/test/fixtures/operation.fixture.ts +20 -0
  247. package/src/test/fixtures/transaction.fixture.ts +22 -0
  248. package/src/types/bridge.ts +33 -0
@@ -1,5 +1,5 @@
1
1
  import network from "@ledgerhq/live-network/network";
2
- import { getAccountTransactions } from "./mirror";
2
+ import { getAccount, getAccountTokens, getAccountTransactions } from "./mirror";
3
3
 
4
4
  jest.mock("@ledgerhq/live-network/network");
5
5
  const mockedNetwork = jest.mocked(network);
@@ -26,10 +26,10 @@ describe("getAccountTransactions", () => {
26
26
 
27
27
  await getAccountTransactions("0.0.1234", null);
28
28
 
29
- const calledUrl = mockedNetwork.mock.calls[0][0].url;
30
- expect(calledUrl).toContain("account.id=0.0.1234");
31
- expect(calledUrl).toContain("limit=100");
32
- expect(calledUrl).toContain("order=desc");
29
+ const requestUrl = mockedNetwork.mock.calls[0][0].url;
30
+ expect(requestUrl).toContain("account.id=0.0.1234");
31
+ expect(requestUrl).toContain("limit=100");
32
+ expect(requestUrl).toContain("order=desc");
33
33
  });
34
34
 
35
35
  test("should keep fetching if links.next is present", async () => {
@@ -72,3 +72,77 @@ describe("getAccountTransactions", () => {
72
72
  expect(mockedNetwork).toHaveBeenCalledTimes(5);
73
73
  });
74
74
  });
75
+
76
+ describe("getAccount", () => {
77
+ beforeEach(() => {
78
+ jest.clearAllMocks();
79
+ });
80
+
81
+ it("should call the correct endpoint and return account data", async () => {
82
+ mockedNetwork.mockResolvedValueOnce(
83
+ makeMockResponse({
84
+ account: "0.0.1234",
85
+ max_automatic_token_associations: 0,
86
+ balance: {
87
+ balance: 1000,
88
+ timestamp: "1749047764.000113442",
89
+ tokens: [],
90
+ },
91
+ }),
92
+ );
93
+
94
+ const result = await getAccount("0.0.1234");
95
+ const requestUrl = mockedNetwork.mock.calls[0][0].url;
96
+
97
+ expect(result.account).toEqual("0.0.1234");
98
+ expect(requestUrl).toContain("/api/v1/accounts/0.0.1234");
99
+ expect(mockedNetwork).toHaveBeenCalledTimes(1);
100
+ });
101
+ });
102
+
103
+ describe("getAccountTokens", () => {
104
+ beforeEach(() => {
105
+ jest.clearAllMocks();
106
+ });
107
+
108
+ it("should return all tokens if only one page is needed", async () => {
109
+ mockedNetwork.mockResolvedValueOnce(
110
+ makeMockResponse({
111
+ tokens: [
112
+ { token_id: "0.0.1001", balance: 10 },
113
+ { token_id: "0.0.1002", balance: 20 },
114
+ ],
115
+ links: { next: null },
116
+ }),
117
+ );
118
+
119
+ const result = await getAccountTokens("0.0.1234");
120
+ const requestUrl = mockedNetwork.mock.calls[0][0].url;
121
+
122
+ expect(result.map(t => t.token_id)).toEqual(["0.0.1001", "0.0.1002"]);
123
+ expect(requestUrl).toContain("/api/v1/accounts/0.0.1234/tokens");
124
+ expect(requestUrl).toContain("limit=100");
125
+ expect(mockedNetwork).toHaveBeenCalledTimes(1);
126
+ });
127
+
128
+ it("should keep fetching if links.next is present and new tokens are returned", async () => {
129
+ mockedNetwork
130
+ .mockResolvedValueOnce(
131
+ makeMockResponse({
132
+ tokens: [{ token_id: "0.0.1001", balance: 10 }],
133
+ links: { next: "/next-1" },
134
+ }),
135
+ )
136
+ .mockResolvedValueOnce(
137
+ makeMockResponse({
138
+ tokens: [{ token_id: "0.0.1002", balance: 20 }],
139
+ links: { next: null },
140
+ }),
141
+ );
142
+
143
+ const result = await getAccountTokens("0.0.1234");
144
+
145
+ expect(result.map(t => t.token_id)).toEqual(["0.0.1001", "0.0.1002"]);
146
+ expect(mockedNetwork).toHaveBeenCalledTimes(2);
147
+ });
148
+ });
package/src/api/mirror.ts CHANGED
@@ -1,12 +1,8 @@
1
- import { AccountId } from "@hashgraph/sdk";
2
1
  import network from "@ledgerhq/live-network/network";
3
- import { Operation, OperationType } from "@ledgerhq/types-live";
4
- import BigNumber from "bignumber.js";
5
2
  import { getEnv } from "@ledgerhq/live-env";
6
- import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
7
- import { getAccountBalance } from "./network";
8
- import { base64ToUrlSafeBase64 } from "../bridge/utils";
9
- import { HederaOperationExtra } from "../types";
3
+ import type { HederaMirrorAccount, HederaMirrorToken, HederaMirrorTransaction } from "./types";
4
+ import { HederaAddAccountError } from "../errors";
5
+ import { LedgerAPI4xx } from "@ledgerhq/errors";
10
6
 
11
7
  const getMirrorApiUrl = (): string => getEnv("API_HEDERA_MIRROR");
12
8
 
@@ -17,45 +13,33 @@ const fetch = (path: string) => {
17
13
  });
18
14
  };
19
15
 
20
- interface HederaMirrorAccount {
21
- accountId: AccountId;
22
- balance: BigNumber;
23
- }
24
-
25
16
  export async function getAccountsForPublicKey(publicKey: string): Promise<HederaMirrorAccount[]> {
26
17
  let r;
27
18
  try {
28
- r = await fetch(`/api/v1/accounts?account.publicKey=${publicKey}&balance=false`);
19
+ r = await fetch(`/api/v1/accounts?account.publicKey=${publicKey}&balance=true&limit=100`);
29
20
  } catch (e: any) {
30
21
  if (e.name === "LedgerAPI4xx") return [];
31
22
  throw e;
32
23
  }
33
- const rawAccounts = r.data.accounts;
34
- const accounts: HederaMirrorAccount[] = [];
35
24
 
36
- for (const raw of rawAccounts) {
37
- const accountBalance = await getAccountBalance(raw.account);
38
-
39
- accounts.push({
40
- accountId: AccountId.fromString(raw.account),
41
- balance: accountBalance.balance,
42
- });
43
- }
25
+ const accounts = r.data.accounts as HederaMirrorAccount[];
44
26
 
45
27
  return accounts;
46
28
  }
47
29
 
48
- interface HederaMirrorTransfer {
49
- account: string;
50
- amount: number;
51
- }
30
+ export async function getAccount(address: string): Promise<HederaMirrorAccount> {
31
+ try {
32
+ const res = await fetch(`/api/v1/accounts/${address}`);
33
+ const account = res.data as HederaMirrorAccount;
34
+
35
+ return account;
36
+ } catch (error) {
37
+ if (error instanceof LedgerAPI4xx && "status" in error && error.status === 404) {
38
+ throw new HederaAddAccountError();
39
+ }
52
40
 
53
- interface HederaMirrorTransaction {
54
- transfers: HederaMirrorTransfer[];
55
- charged_tx_fee: string;
56
- transaction_hash: string;
57
- consensus_timestamp: string;
58
- transaction_id: string;
41
+ throw error;
42
+ }
59
43
  }
60
44
 
61
45
  export async function getAccountTransactions(
@@ -88,85 +72,20 @@ export async function getAccountTransactions(
88
72
  return transactions;
89
73
  }
90
74
 
91
- export async function getOperationsForAccount(
92
- ledgerAccountId: string,
93
- address: string,
94
- latestOperationTimestamp: string | null,
95
- ): Promise<Operation[]> {
96
- const rawOperations = await getAccountTransactions(address, latestOperationTimestamp);
97
- const operations: Operation[] = [];
98
-
99
- for (const raw of rawOperations) {
100
- const { consensus_timestamp, transaction_id } = raw;
101
- const timestamp = new Date(parseInt(consensus_timestamp.split(".")[0], 10) * 1000);
102
- const senders: string[] = [];
103
- const recipients: string[] = [];
104
- const fee = new BigNumber(raw.charged_tx_fee);
105
- let value = new BigNumber(0);
106
- let type: OperationType = "NONE";
107
-
108
- for (let i = raw.transfers.length - 1; i >= 0; i--) {
109
- const transfer = raw.transfers[i];
110
- const amount = new BigNumber(transfer.amount);
111
- const account = AccountId.fromString(transfer.account);
112
-
113
- if (transfer.account === address) {
114
- if (amount.isNegative()) {
115
- value = amount.abs();
116
- type = "OUT";
117
- } else {
118
- value = amount;
119
- type = "IN";
120
- }
121
- }
122
-
123
- if (amount.isNegative()) {
124
- senders.push(transfer.account);
125
- } else {
126
- if (account.shard.eq(0) && account.realm.eq(0)) {
127
- if (account.num.lt(100)) {
128
- // account is a node, only add to list if we have none
129
- if (recipients.length === 0) {
130
- recipients.push(transfer.account);
131
- }
132
- } else if (account.num.lt(1000)) {
133
- // account is a system account that is not a node
134
- // do NOT add
135
- } else {
136
- recipients.push(transfer.account);
137
- }
138
- } else {
139
- recipients.push(transfer.account);
140
- }
141
- }
142
- }
75
+ export async function getAccountTokens(address: string): Promise<HederaMirrorToken[]> {
76
+ const tokens: HederaMirrorToken[] = [];
77
+ const params = new URLSearchParams({
78
+ limit: "100",
79
+ });
143
80
 
144
- // NOTE: earlier addresses are the "fee" addresses
145
- recipients.reverse();
146
- senders.reverse();
147
-
148
- const hash = base64ToUrlSafeBase64(raw.transaction_hash);
149
-
150
- operations.push({
151
- value,
152
- date: timestamp,
153
- // NOTE: there are no "blocks" in hedera
154
- // Set a value just so that it's considered confirmed according to isConfirmedOperation
155
- blockHeight: 5,
156
- blockHash: null,
157
- extra: {
158
- consensusTimestamp: consensus_timestamp,
159
- transactionId: transaction_id,
160
- } satisfies HederaOperationExtra,
161
- fee,
162
- hash,
163
- recipients,
164
- senders,
165
- accountId: ledgerAccountId,
166
- id: encodeOperationId(ledgerAccountId, hash, type),
167
- type,
168
- });
81
+ let nextUrl = `/api/v1/accounts/${address}/tokens?${params.toString()}`;
82
+
83
+ while (nextUrl) {
84
+ const res = await fetch(nextUrl);
85
+ const newTokens = res.data.tokens as HederaMirrorToken[];
86
+ tokens.push(...newTokens);
87
+ nextUrl = res.data.links.next;
169
88
  }
170
89
 
171
- return operations;
90
+ return tokens;
172
91
  }
@@ -1,4 +1,5 @@
1
1
  import BigNumber from "bignumber.js";
2
+ import invariant from "invariant";
2
3
  import type { Transaction as HederaTransaction, TransactionResponse } from "@hashgraph/sdk";
3
4
  import {
4
5
  Client,
@@ -8,27 +9,33 @@ import {
8
9
  TransactionId,
9
10
  AccountBalanceQuery,
10
11
  HbarUnit,
12
+ TokenAssociateTransaction,
11
13
  } from "@hashgraph/sdk";
12
- import { Account } from "@ledgerhq/types-live";
14
+ import type { Account, TokenAccount } from "@ledgerhq/types-live";
15
+ import { findSubAccountById, isTokenAccount } from "@ledgerhq/coin-framework/account/helpers";
13
16
  import { HederaAddAccountError } from "../errors";
14
17
  import { Transaction } from "../types";
18
+ import { isTokenAssociateTransaction } from "../logic";
15
19
 
16
20
  export function broadcastTransaction(transaction: HederaTransaction): Promise<TransactionResponse> {
17
21
  return transaction.execute(getClient());
18
22
  }
19
23
 
20
- export async function buildUnsignedTransaction({
24
+ // https://github.com/LedgerHQ/ledger-live/pull/72/commits/1e942687d4301660e43e0c4b5419fcfa2733b290
25
+ const nodeAccountIds: AccountId[] = [new AccountId(3)];
26
+
27
+ async function buildUnsignedCoinTransaction({
21
28
  account,
22
29
  transaction,
23
30
  }: {
24
31
  account: Account;
25
32
  transaction: Transaction;
26
33
  }): Promise<TransferTransaction> {
27
- const hbarAmount = Hbar.fromTinybars(transaction.amount);
28
34
  const accountId = account.freshAddress;
35
+ const hbarAmount = Hbar.fromTinybars(transaction.amount);
29
36
 
30
37
  return new TransferTransaction()
31
- .setNodeAccountIds([new AccountId(3)])
38
+ .setNodeAccountIds(nodeAccountIds)
32
39
  .setTransactionId(TransactionId.generate(accountId))
33
40
  .setTransactionMemo(transaction.memo ?? "")
34
41
  .addHbarTransfer(accountId, hbarAmount.negated())
@@ -36,6 +43,66 @@ export async function buildUnsignedTransaction({
36
43
  .freeze();
37
44
  }
38
45
 
46
+ async function buildUnsignedTokenTransaction({
47
+ account,
48
+ tokenAccount,
49
+ transaction,
50
+ }: {
51
+ account: Account;
52
+ tokenAccount: TokenAccount;
53
+ transaction: Transaction;
54
+ }): Promise<TransferTransaction> {
55
+ const accountId = account.freshAddress;
56
+ const tokenId = tokenAccount.token.contractAddress;
57
+
58
+ return new TransferTransaction()
59
+ .setNodeAccountIds(nodeAccountIds)
60
+ .setTransactionId(TransactionId.generate(accountId))
61
+ .setTransactionMemo(transaction.memo ?? "")
62
+ .addTokenTransfer(tokenId, accountId, transaction.amount.negated().toNumber())
63
+ .addTokenTransfer(tokenId, transaction.recipient, transaction.amount.toNumber())
64
+ .freeze();
65
+ }
66
+
67
+ async function buildTokenAssociateTransaction({
68
+ account,
69
+ transaction,
70
+ }: {
71
+ account: Account;
72
+ transaction: Transaction;
73
+ }): Promise<TokenAssociateTransaction> {
74
+ invariant(isTokenAssociateTransaction(transaction), "invalid transaction properties");
75
+
76
+ const accountId = account.freshAddress;
77
+
78
+ return new TokenAssociateTransaction()
79
+ .setNodeAccountIds(nodeAccountIds)
80
+ .setTransactionId(TransactionId.generate(accountId))
81
+ .setTransactionMemo(transaction.memo ?? "")
82
+ .setAccountId(accountId)
83
+ .setTokenIds([transaction.properties.token.contractAddress])
84
+ .freeze();
85
+ }
86
+
87
+ export async function buildUnsignedTransaction({
88
+ account,
89
+ transaction,
90
+ }: {
91
+ account: Account;
92
+ transaction: Transaction;
93
+ }): Promise<TransferTransaction | TokenAssociateTransaction> {
94
+ const subAccount = findSubAccountById(account, transaction?.subAccountId || "");
95
+ const isTokenTransaction = isTokenAccount(subAccount);
96
+
97
+ if (isTokenAssociateTransaction(transaction)) {
98
+ return buildTokenAssociateTransaction({ account, transaction });
99
+ } else if (isTokenTransaction) {
100
+ return buildUnsignedTokenTransaction({ account, tokenAccount: subAccount, transaction });
101
+ } else {
102
+ return buildUnsignedCoinTransaction({ account, transaction });
103
+ }
104
+ }
105
+
39
106
  export interface AccountBalance {
40
107
  balance: BigNumber;
41
108
  }
@@ -0,0 +1,48 @@
1
+ type FreezeStatus = "NOT_APPLICABLE" | "FROZEN" | "UNFROZEN";
2
+
3
+ type KycStatus = "NOT_APPLICABLE" | "GRANTED" | "REVOKED";
4
+
5
+ export interface HederaMirrorCoinTransfer {
6
+ account: string;
7
+ amount: number;
8
+ }
9
+
10
+ export interface HederaMirrorTokenTransfer {
11
+ token_id: string;
12
+ account: string;
13
+ amount: number;
14
+ is_approval?: boolean;
15
+ }
16
+
17
+ export interface HederaMirrorTransaction {
18
+ transfers: HederaMirrorCoinTransfer[];
19
+ token_transfers: HederaMirrorTokenTransfer[];
20
+ charged_tx_fee: string;
21
+ transaction_hash: string;
22
+ consensus_timestamp: string;
23
+ result: string;
24
+ name: string;
25
+ }
26
+
27
+ export interface HederaMirrorToken {
28
+ automatic_association: boolean;
29
+ balance: number;
30
+ created_timestamp: string;
31
+ decimals: number;
32
+ token_id: string;
33
+ freeze_status: FreezeStatus;
34
+ kyc_status: KycStatus;
35
+ }
36
+
37
+ export interface HederaMirrorAccount {
38
+ account: string;
39
+ max_automatic_token_associations: number;
40
+ balance: {
41
+ balance: number;
42
+ timestamp: string;
43
+ tokens: {
44
+ token_id: string;
45
+ balance: number;
46
+ }[];
47
+ };
48
+ }
@@ -0,0 +1,150 @@
1
+ import BigNumber from "bignumber.js";
2
+ import { AccountId } from "@hashgraph/sdk";
3
+ import type { Operation, OperationType } from "@ledgerhq/types-live";
4
+ import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
5
+ import { encodeTokenAccountId } from "@ledgerhq/coin-framework/account";
6
+ import { findTokenByAddressInCurrency } from "@ledgerhq/cryptoassets";
7
+ import type { HederaMirrorTokenTransfer, HederaMirrorCoinTransfer } from "./types";
8
+ import { getAccountTransactions } from "./mirror";
9
+ import { base64ToUrlSafeBase64 } from "../bridge/utils";
10
+
11
+ function isValidRecipient(accountId: AccountId, recipients: string[]): boolean {
12
+ if (accountId.shard.eq(0) && accountId.realm.eq(0)) {
13
+ // account is a node, only add to list if we have none
14
+ if (accountId.num.lt(100)) {
15
+ return recipients.length === 0;
16
+ }
17
+
18
+ // account is a system account that is not a node, do NOT add
19
+ if (accountId.num.lt(1000)) {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ return true;
25
+ }
26
+
27
+ export function parseTransfers(
28
+ mirrorTransfers: (HederaMirrorCoinTransfer | HederaMirrorTokenTransfer)[],
29
+ address: string,
30
+ ): Pick<Operation, "type" | "value" | "senders" | "recipients"> {
31
+ let value = new BigNumber(0);
32
+ let type: OperationType = "NONE";
33
+
34
+ const senders: string[] = [];
35
+ const recipients: string[] = [];
36
+
37
+ for (const transfer of mirrorTransfers) {
38
+ const amount = new BigNumber(transfer.amount);
39
+ const accountId = AccountId.fromString(transfer.account);
40
+
41
+ if (transfer.account === address) {
42
+ value = amount.abs();
43
+ type = amount.isNegative() ? "OUT" : "IN";
44
+ }
45
+
46
+ if (amount.isNegative()) {
47
+ senders.push(transfer.account);
48
+ } else if (isValidRecipient(accountId, recipients)) {
49
+ recipients.push(transfer.account);
50
+ }
51
+ }
52
+
53
+ // NOTE: earlier addresses are the "fee" addresses
54
+ senders.reverse();
55
+ recipients.reverse();
56
+
57
+ return {
58
+ type,
59
+ value,
60
+ senders,
61
+ recipients,
62
+ };
63
+ }
64
+
65
+ export async function getOperationsForAccount(
66
+ ledgerAccountId: string,
67
+ address: string,
68
+ latestOperationTimestamp: string | null,
69
+ ): Promise<{
70
+ coinOperations: Operation[];
71
+ tokenOperations: Operation[];
72
+ }> {
73
+ const mirrorTransactions = await getAccountTransactions(address, latestOperationTimestamp);
74
+ const coinOperations: Operation[] = [];
75
+ const tokenOperations: Operation[] = [];
76
+
77
+ for (const rawTx of mirrorTransactions) {
78
+ const timestamp = new Date(parseInt(rawTx.consensus_timestamp.split(".")[0], 10) * 1000);
79
+ const hash = base64ToUrlSafeBase64(rawTx.transaction_hash);
80
+ const fee = new BigNumber(rawTx.charged_tx_fee);
81
+ const tokenTransfers = rawTx.token_transfers ?? [];
82
+ const transfers = rawTx.transfers ?? [];
83
+ const hasFailed = rawTx.result !== "SUCCESS";
84
+
85
+ if (tokenTransfers.length > 0) {
86
+ const tokenId = rawTx.token_transfers[0].token_id;
87
+ const token = findTokenByAddressInCurrency(tokenId, "hedera");
88
+ if (!token) continue;
89
+
90
+ const encodedTokenId = encodeTokenAccountId(ledgerAccountId, token);
91
+ const { type, value, senders, recipients } = parseTransfers(rawTx.token_transfers, address);
92
+
93
+ // add main FEES coin operation for send token transfer
94
+ if (type === "OUT") {
95
+ coinOperations.push({
96
+ id: encodeOperationId(ledgerAccountId, hash, "FEES"),
97
+ accountId: ledgerAccountId,
98
+ type: "FEES",
99
+ value: fee,
100
+ recipients,
101
+ senders,
102
+ hash,
103
+ fee,
104
+ date: timestamp,
105
+ blockHeight: 5,
106
+ blockHash: null,
107
+ hasFailed,
108
+ extra: { consensusTimestamp: rawTx.consensus_timestamp },
109
+ });
110
+ }
111
+
112
+ tokenOperations.push({
113
+ id: encodeOperationId(encodedTokenId, hash, type),
114
+ accountId: encodedTokenId,
115
+ type,
116
+ value,
117
+ recipients,
118
+ senders,
119
+ hash,
120
+ fee,
121
+ date: timestamp,
122
+ blockHeight: 5,
123
+ blockHash: null,
124
+ hasFailed,
125
+ extra: { consensusTimestamp: rawTx.consensus_timestamp },
126
+ });
127
+ } else if (transfers.length > 0) {
128
+ const { type, value, senders, recipients } = parseTransfers(rawTx.transfers, address);
129
+ const operationType = rawTx.name === "TOKENASSOCIATE" ? "ASSOCIATE_TOKEN" : type;
130
+
131
+ coinOperations.push({
132
+ id: encodeOperationId(ledgerAccountId, hash, operationType),
133
+ accountId: ledgerAccountId,
134
+ type: operationType,
135
+ value,
136
+ recipients,
137
+ senders,
138
+ hash,
139
+ fee,
140
+ date: timestamp,
141
+ blockHeight: 5,
142
+ blockHash: null,
143
+ hasFailed,
144
+ extra: { consensusTimestamp: rawTx.consensus_timestamp },
145
+ });
146
+ }
147
+ }
148
+
149
+ return { coinOperations, tokenOperations };
150
+ }
@@ -4,6 +4,7 @@ import { patchOperationWithHash } from "@ledgerhq/coin-framework/operation";
4
4
  import { base64ToUrlSafeBase64, patchOperationWithExtra } from "./utils";
5
5
  import { HederaOperationExtra, Transaction } from "../types";
6
6
  import { broadcastTransaction } from "../api/network";
7
+ import { isValidExtra } from "../logic";
7
8
 
8
9
  export const broadcast: AccountBridge<Transaction>["broadcast"] = async ({ signedOperation }) => {
9
10
  const { signature, operation } = signedOperation;
@@ -16,6 +17,7 @@ export const broadcast: AccountBridge<Transaction>["broadcast"] = async ({ signe
16
17
  const base64Hash = Buffer.from(response.transactionHash).toString("base64");
17
18
  const base64HashUrlSafe = base64ToUrlSafeBase64(base64Hash);
18
19
  const extra: HederaOperationExtra = {
20
+ ...(isValidExtra(operation.extra) ? operation.extra : {}),
19
21
  transactionId: response.transactionId.toString(),
20
22
  };
21
23
 
@@ -0,0 +1,88 @@
1
+ import BigNumber from "bignumber.js";
2
+ import { getMockedAccount, getMockedTokenAccount } from "../test/fixtures/account.fixture";
3
+ import { getMockedTokenCurrency } from "../test/fixtures/currency.fixture";
4
+ import { getMockedTransaction } from "../test/fixtures/transaction.fixture";
5
+ import { buildOptimisticOperation } from "./buildOptimisticOperation";
6
+ import { getEstimatedFees } from "./utils";
7
+ import { HEDERA_OPERATION_TYPES, HEDERA_TRANSACTION_KINDS } from "../constants";
8
+
9
+ describe("buildOptimisticOperation", () => {
10
+ let estimatedFees: Record<"crypto" | "associate", BigNumber>;
11
+
12
+ beforeAll(async () => {
13
+ const mockedAccount = getMockedAccount();
14
+ const [crypto, associate] = await Promise.all([
15
+ getEstimatedFees(mockedAccount, HEDERA_OPERATION_TYPES.CryptoTransfer),
16
+ getEstimatedFees(mockedAccount, HEDERA_OPERATION_TYPES.TokenAssociate),
17
+ ]);
18
+
19
+ estimatedFees = { crypto, associate };
20
+ });
21
+
22
+ test("builds optimistic operation for token association", async () => {
23
+ const mockedAccount = getMockedAccount();
24
+ const mockedToken = getMockedTokenCurrency();
25
+ const mockedTransaction = getMockedTransaction({
26
+ amount: new BigNumber(0),
27
+ recipient: "0.0.1234",
28
+ properties: {
29
+ name: HEDERA_TRANSACTION_KINDS.TokenAssociate.name,
30
+ token: mockedToken,
31
+ },
32
+ });
33
+
34
+ const op = await buildOptimisticOperation({
35
+ account: mockedAccount,
36
+ transaction: mockedTransaction,
37
+ });
38
+
39
+ expect(op.type).toBe("ASSOCIATE_TOKEN");
40
+ expect(op.extra).toEqual({ associatedTokenId: mockedToken.contractAddress });
41
+ expect(op.fee).toEqual(estimatedFees.associate);
42
+ expect(op.senders).toContain(mockedAccount.freshAddress.toString());
43
+ expect(op.recipients).toContain("0.0.1234");
44
+ });
45
+
46
+ test("builds optimistic operation for coin", async () => {
47
+ const mockedAccount = getMockedAccount();
48
+ const mockedTransaction = getMockedTransaction({
49
+ amount: new BigNumber(123),
50
+ recipient: "0.0.5678",
51
+ });
52
+
53
+ const op = await buildOptimisticOperation({
54
+ account: mockedAccount,
55
+ transaction: mockedTransaction,
56
+ });
57
+
58
+ expect(op.type).toBe("OUT");
59
+ expect(op.fee).toEqual(estimatedFees.crypto);
60
+ expect(op.value).toEqual(new BigNumber(123));
61
+ expect(op.senders).toContain(mockedAccount.freshAddress.toString());
62
+ expect(op.recipients).toContain("0.0.5678");
63
+ });
64
+
65
+ test("builds optimistic operation for token", async () => {
66
+ const mockedTokenCurrency = getMockedTokenCurrency();
67
+ const tokenAccount = getMockedTokenAccount(mockedTokenCurrency);
68
+ const parentAccount = getMockedAccount({ subAccounts: [tokenAccount] });
69
+ const mockedTransaction = getMockedTransaction({
70
+ subAccountId: tokenAccount.id,
71
+ amount: new BigNumber(123),
72
+ recipient: "0.0.9999",
73
+ });
74
+
75
+ const op = await buildOptimisticOperation({
76
+ account: parentAccount,
77
+ transaction: mockedTransaction,
78
+ });
79
+ const subOp = op.subOperations![0];
80
+
81
+ expect(op.type).toBe("FEES");
82
+ expect(op.subOperations).toHaveLength(1);
83
+ expect(subOp.type).toBe("OUT");
84
+ expect(subOp.value).toEqual(new BigNumber(123));
85
+ expect(subOp.accountId).toBe(tokenAccount.id);
86
+ expect(subOp.recipients).toContain("0.0.9999");
87
+ });
88
+ });