@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,55 +1,141 @@
1
1
  import BigNumber from "bignumber.js";
2
- import type { Account, Operation } from "@ledgerhq/types-live";
2
+ import murmurhash from "imurmurhash";
3
+ import invariant from "invariant";
4
+ import type { Account, Operation, TokenAccount } from "@ledgerhq/types-live";
3
5
  import cvsApi from "@ledgerhq/live-countervalues/api/index";
4
- import { getFiatCurrencyByTicker } from "@ledgerhq/cryptoassets";
6
+ import {
7
+ findTokenByAddressInCurrency,
8
+ getFiatCurrencyByTicker,
9
+ listTokensForCryptoCurrency,
10
+ } from "@ledgerhq/cryptoassets";
11
+ import {
12
+ decodeTokenAccountId,
13
+ emptyHistoryCache,
14
+ encodeTokenAccountId,
15
+ findSubAccountById,
16
+ isTokenAccount,
17
+ } from "@ledgerhq/coin-framework/account";
18
+ import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
19
+ import type { CryptoCurrency, Currency, TokenCurrency } from "@ledgerhq/types-cryptoassets";
20
+ import { mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers";
21
+ import { makeLRUCache, seconds } from "@ledgerhq/live-network/cache";
5
22
  import { estimateMaxSpendable } from "./estimateMaxSpendable";
6
23
  import type { HederaOperationExtra, Transaction } from "../types";
24
+ import { getAccount } from "../api/mirror";
25
+ import type { HederaMirrorToken } from "../api/types";
26
+ import { isTokenAssociateTransaction, isValidExtra } from "../logic";
27
+ import { BASE_USD_FEE_BY_OPERATION_TYPE, HEDERA_OPERATION_TYPES } from "../constants";
7
28
 
8
- export const estimatedFeeSafetyRate = 2;
29
+ const ESTIMATED_FEE_SAFETY_RATE = 2;
9
30
 
10
- export async function getEstimatedFees(account: Account): Promise<BigNumber> {
31
+ // note: this is currently called frequently by getTransactionStatus; LRU cache prevents duplicated requests
32
+ export const getCurrencyToUSDRate = makeLRUCache(
33
+ async (currency: Currency) => {
34
+ try {
35
+ const [rate] = await cvsApi.fetchLatest([
36
+ {
37
+ from: currency,
38
+ to: getFiatCurrencyByTicker("USD"),
39
+ startDate: new Date(),
40
+ },
41
+ ]);
42
+
43
+ invariant(rate, "no value returned from cvs api");
44
+
45
+ return new BigNumber(rate);
46
+ } catch {
47
+ return null;
48
+ }
49
+ },
50
+ currency => currency.ticker,
51
+ seconds(3),
52
+ );
53
+
54
+ export const getEstimatedFees = async (
55
+ account: Account,
56
+ operationType: HEDERA_OPERATION_TYPES,
57
+ ): Promise<BigNumber> => {
11
58
  try {
12
- const data = await cvsApi.fetchLatest([
13
- {
14
- from: account.currency,
15
- to: getFiatCurrencyByTicker("USD"),
16
- startDate: new Date(),
17
- },
18
- ]);
59
+ const usdRate = await getCurrencyToUSDRate(account.currency);
19
60
 
20
- if (data[0]) {
21
- return new BigNumber(10000)
22
- .dividedBy(new BigNumber(data[0]))
61
+ if (usdRate) {
62
+ return new BigNumber(BASE_USD_FEE_BY_OPERATION_TYPE[operationType])
63
+ .dividedBy(new BigNumber(usdRate))
23
64
  .integerValue(BigNumber.ROUND_CEIL)
24
- .multipliedBy(estimatedFeeSafetyRate);
65
+ .multipliedBy(ESTIMATED_FEE_SAFETY_RATE);
25
66
  }
26
67
  // eslint-disable-next-line no-empty
27
68
  } catch {}
28
69
 
29
70
  // as fees are based on a currency conversion, we stay
30
71
  // on the safe side here and double the estimate for "max spendable"
31
- return new BigNumber("150200").multipliedBy(estimatedFeeSafetyRate); // 0.001502 ℏ (as of 2023-03-14)
72
+ return new BigNumber("150200").multipliedBy(ESTIMATED_FEE_SAFETY_RATE); // 0.001502 ℏ (as of 2023-03-14)
73
+ };
74
+
75
+ interface CalculateAmountResult {
76
+ amount: BigNumber;
77
+ totalSpent: BigNumber;
32
78
  }
33
79
 
34
- export async function calculateAmount({
80
+ const calculateCoinAmount = async ({
35
81
  account,
36
82
  transaction,
83
+ operationType,
37
84
  }: {
38
85
  account: Account;
39
86
  transaction: Transaction;
40
- }): Promise<{
41
- amount: BigNumber;
42
- totalSpent: BigNumber;
43
- }> {
87
+ operationType: HEDERA_OPERATION_TYPES;
88
+ }): Promise<CalculateAmountResult> => {
89
+ const estimatedFees = await getEstimatedFees(account, operationType);
44
90
  const amount = transaction.useAllAmount
45
- ? await estimateMaxSpendable({ account })
91
+ ? await estimateMaxSpendable({ account, transaction })
46
92
  : transaction.amount;
47
93
 
48
94
  return {
49
95
  amount,
50
- totalSpent: amount.plus(await getEstimatedFees(account)),
96
+ totalSpent: amount.plus(estimatedFees),
51
97
  };
52
- }
98
+ };
99
+
100
+ const calculateTokenAmount = async ({
101
+ account,
102
+ tokenAccount,
103
+ transaction,
104
+ }: {
105
+ account: Account;
106
+ tokenAccount: TokenAccount;
107
+ transaction: Transaction;
108
+ }): Promise<CalculateAmountResult> => {
109
+ const amount = transaction.useAllAmount
110
+ ? await estimateMaxSpendable({ account: tokenAccount, parentAccount: account, transaction })
111
+ : transaction.amount;
112
+
113
+ return {
114
+ amount,
115
+ totalSpent: amount,
116
+ };
117
+ };
118
+
119
+ export const calculateAmount = ({
120
+ account,
121
+ transaction,
122
+ }: {
123
+ account: Account;
124
+ transaction: Transaction;
125
+ }): Promise<CalculateAmountResult> => {
126
+ const subAccount = findSubAccountById(account, transaction?.subAccountId || "");
127
+ const isTokenTransaction = isTokenAccount(subAccount);
128
+
129
+ if (isTokenTransaction) {
130
+ return calculateTokenAmount({ account, tokenAccount: subAccount, transaction });
131
+ }
132
+
133
+ const operationType: HEDERA_OPERATION_TYPES = isTokenAssociateTransaction(transaction)
134
+ ? HEDERA_OPERATION_TYPES.TokenAssociate
135
+ : HEDERA_OPERATION_TYPES.CryptoTransfer;
136
+
137
+ return calculateCoinAmount({ account, transaction, operationType });
138
+ };
53
139
 
54
140
  // NOTE: convert from the non-url-safe version of base64 to the url-safe version (that the explorer uses)
55
141
  export function base64ToUrlSafeBase64(data: string): string {
@@ -60,6 +146,300 @@ export function base64ToUrlSafeBase64(data: string): string {
60
146
  return data.replace(/\//g, "_").replace(/\+/g, "-");
61
147
  }
62
148
 
149
+ const simpleSyncHashMemoize: Record<string, string> = {};
150
+
151
+ export const getSyncHash = (
152
+ currency: CryptoCurrency,
153
+ blacklistedTokenIds: string[] = [],
154
+ ): string => {
155
+ const tokens = listTokensForCryptoCurrency(currency);
156
+
157
+ const stringToHash =
158
+ currency.id +
159
+ tokens.map(token => token.id + token.contractAddress + token.name + token.ticker).join("") +
160
+ blacklistedTokenIds.join("");
161
+
162
+ if (!simpleSyncHashMemoize[stringToHash]) {
163
+ simpleSyncHashMemoize[stringToHash] = `0x${murmurhash(stringToHash).result().toString(16)}`;
164
+ }
165
+
166
+ return simpleSyncHashMemoize[stringToHash];
167
+ };
168
+
169
+ export const getSubAccounts = async (
170
+ accountId: string,
171
+ lastTokenOperations: Operation[],
172
+ mirrorTokens: HederaMirrorToken[],
173
+ ): Promise<TokenAccount[]> => {
174
+ // Creating a Map of Operations by TokenCurrencies in order to know which TokenAccounts should be synced as well
175
+ const operationsByToken = lastTokenOperations.reduce<Map<TokenCurrency, Operation[]>>(
176
+ (acc, tokenOperation) => {
177
+ const { token } = decodeTokenAccountId(tokenOperation.accountId);
178
+ if (!token) return acc;
179
+
180
+ const isTokenListedInCAL = findTokenByAddressInCurrency(
181
+ token.contractAddress,
182
+ token.parentCurrency.id,
183
+ );
184
+ if (!isTokenListedInCAL) return acc;
185
+
186
+ if (!acc.has(token)) {
187
+ acc.set(token, []);
188
+ }
189
+
190
+ acc.get(token)?.push(tokenOperation);
191
+
192
+ return acc;
193
+ },
194
+ new Map<TokenCurrency, Operation[]>(),
195
+ );
196
+
197
+ const subAccounts: TokenAccount[] = [];
198
+
199
+ // extract token accounts from existing operations
200
+ for (const [token, tokenOperations] of operationsByToken.entries()) {
201
+ const parentAccountId = accountId;
202
+ const rawBalance = mirrorTokens.find(t => t.token_id === token.contractAddress)?.balance;
203
+ const balance = rawBalance !== undefined ? new BigNumber(rawBalance) : null;
204
+
205
+ if (!balance) {
206
+ continue;
207
+ }
208
+
209
+ subAccounts.push({
210
+ type: "TokenAccount",
211
+ id: encodeTokenAccountId(parentAccountId, token),
212
+ parentId: parentAccountId,
213
+ token,
214
+ balance,
215
+ spendableBalance: balance,
216
+ creationDate:
217
+ tokenOperations.length > 0 ? tokenOperations[tokenOperations.length - 1].date : new Date(),
218
+ operations: tokenOperations,
219
+ operationsCount: tokenOperations.length,
220
+ pendingOperations: [],
221
+ balanceHistoryCache: emptyHistoryCache,
222
+ swapHistory: [],
223
+ });
224
+ }
225
+
226
+ // extract token accounts existing in the account's balance, but with no recorded operations yet
227
+ // e.g. tokens added via association flow, without any subsequent activity
228
+ for (const rawToken of mirrorTokens) {
229
+ const parentAccountId = accountId;
230
+ const rawBalance = rawToken.balance;
231
+ const balance = new BigNumber(rawBalance);
232
+ const token = findTokenByAddressInCurrency(rawToken.token_id, "hedera");
233
+
234
+ if (!token) {
235
+ continue;
236
+ }
237
+
238
+ const id = encodeTokenAccountId(parentAccountId, token);
239
+
240
+ if (subAccounts.some(a => a.id === id)) {
241
+ continue;
242
+ }
243
+
244
+ subAccounts.push({
245
+ type: "TokenAccount",
246
+ id: encodeTokenAccountId(parentAccountId, token),
247
+ parentId: parentAccountId,
248
+ token,
249
+ balance,
250
+ spendableBalance: balance,
251
+ creationDate: new Date(parseFloat(rawToken.created_timestamp) * 1000),
252
+ operations: [],
253
+ operationsCount: 0,
254
+ pendingOperations: [],
255
+ balanceHistoryCache: emptyHistoryCache,
256
+ swapHistory: [],
257
+ });
258
+ }
259
+
260
+ return subAccounts;
261
+ };
262
+
263
+ type CoinOperationForOrphanChildOperation = Operation & Required<Pick<Operation, "subOperations">>;
264
+
265
+ // create NONE coin operation that will be a parent of an orphan child operation
266
+ const makeCoinOperationForOrphanChildOperation = (
267
+ childOperation: Operation,
268
+ ): CoinOperationForOrphanChildOperation => {
269
+ const type = "NONE";
270
+ const { accountId } = decodeTokenAccountId(childOperation.accountId);
271
+ const id = encodeOperationId(accountId, childOperation.hash, type);
272
+
273
+ return {
274
+ id,
275
+ hash: childOperation.hash,
276
+ type,
277
+ value: new BigNumber(0),
278
+ fee: new BigNumber(0),
279
+ senders: [],
280
+ recipients: [],
281
+ blockHeight: childOperation.blockHeight,
282
+ blockHash: childOperation.blockHash,
283
+ transactionSequenceNumber: childOperation.transactionSequenceNumber,
284
+ subOperations: [],
285
+ nftOperations: [],
286
+ internalOperations: [],
287
+ accountId: "",
288
+ date: childOperation.date,
289
+ extra: {},
290
+ };
291
+ };
292
+
293
+ // this util handles:
294
+ // - linking sub operations with coin operations, e.g. token transfer with fee payment
295
+ // - if possible, assigning `extra.associatedTokenId = mirrorToken.tokenId` based on operation's consensus timestamp
296
+ export const prepareOperations = (
297
+ coinOperations: Operation[],
298
+ tokenOperations: Operation[],
299
+ mirrorTokens: HederaMirrorToken[],
300
+ ): Operation[] => {
301
+ const preparedCoinOperations = coinOperations.map(op => ({ ...op }));
302
+ const preparedTokenOperations = tokenOperations.map(op => ({ ...op }));
303
+
304
+ // loop through coin operations to:
305
+ // - enrich ASSOCIATE_TOKEN operations with extra.associatedTokenId
306
+ // - prepare a map of hash => operations
307
+ const coinOperationsByHash: Record<string, CoinOperationForOrphanChildOperation[]> = {};
308
+ preparedCoinOperations.forEach(op => {
309
+ const extra = isValidExtra(op.extra) ? op.extra : null;
310
+
311
+ if (op.type === "ASSOCIATE_TOKEN" && extra?.consensusTimestamp) {
312
+ const relatedMirrorToken = mirrorTokens.find(t => {
313
+ return t.created_timestamp === extra.consensusTimestamp;
314
+ });
315
+
316
+ if (relatedMirrorToken) {
317
+ op.extra = {
318
+ ...extra,
319
+ associatedTokenId: relatedMirrorToken.token_id,
320
+ } satisfies HederaOperationExtra;
321
+ }
322
+ }
323
+
324
+ if (!coinOperationsByHash[op.hash]) {
325
+ coinOperationsByHash[op.hash] = [];
326
+ }
327
+
328
+ op.subOperations = [];
329
+ coinOperationsByHash[op.hash].push(op as CoinOperationForOrphanChildOperation);
330
+ });
331
+
332
+ // loop through token operations to potentially copy them as a child operation of a coin operation
333
+ for (const tokenOperation of preparedTokenOperations) {
334
+ const { token } = decodeTokenAccountId(tokenOperation.accountId);
335
+ if (!token) continue;
336
+
337
+ let mainOperations = coinOperationsByHash[tokenOperation.hash];
338
+
339
+ if (!mainOperations?.length) {
340
+ const noneOperation = makeCoinOperationForOrphanChildOperation(tokenOperation);
341
+ mainOperations = [noneOperation];
342
+ preparedCoinOperations.push(noneOperation);
343
+ }
344
+
345
+ // ugly loop in loop but in theory, this can only be a 2 elements array maximum in the case of a self send
346
+ for (const mainOperation of mainOperations) {
347
+ mainOperation.subOperations.push(tokenOperation);
348
+ }
349
+ }
350
+
351
+ return preparedCoinOperations;
352
+ };
353
+
354
+ /**
355
+ * List of properties of a sub account that can be updated when 2 "identical" accounts are found
356
+ */
357
+ const updatableSubAccountProperties = [
358
+ { name: "balance", isOps: false },
359
+ { name: "spendableBalance", isOps: false },
360
+ { name: "balanceHistoryCache", isOps: false },
361
+ { name: "operations", isOps: true },
362
+ { name: "pendingOperations", isOps: true },
363
+ ] as const satisfies { name: string; isOps: boolean }[];
364
+
365
+ /**
366
+ * In charge of smartly merging sub accounts while maintaining references as much as possible
367
+ */
368
+ export const mergeSubAccounts = (
369
+ initialAccount: Account | undefined,
370
+ newSubAccounts: TokenAccount[],
371
+ ): Array<TokenAccount> => {
372
+ const oldSubAccounts: Array<TokenAccount> | undefined = initialAccount?.subAccounts;
373
+
374
+ if (!oldSubAccounts) {
375
+ return newSubAccounts;
376
+ }
377
+
378
+ // map of already existing sub accounts by id
379
+ const oldSubAccountsById: Record<string, TokenAccount> = {};
380
+ for (const oldSubAccount of oldSubAccounts) {
381
+ oldSubAccountsById[oldSubAccount.id] = oldSubAccount;
382
+ }
383
+
384
+ // looping through new sub accounts to compare them with already existing ones
385
+ // already existing will be updated if necessary (see `updatableSubAccountProperties`)
386
+ // new sub accounts will be added/pushed after already existing
387
+ const newSubAccountsToAdd: TokenAccount[] = [];
388
+ for (const newSubAccount of newSubAccounts) {
389
+ const duplicatedAccount: TokenAccount | undefined = oldSubAccountsById[newSubAccount.id];
390
+
391
+ if (!duplicatedAccount) {
392
+ newSubAccountsToAdd.push(newSubAccount);
393
+ continue;
394
+ }
395
+
396
+ const updates: Partial<TokenAccount> = {};
397
+ for (const { name, isOps } of updatableSubAccountProperties) {
398
+ if (!isOps) {
399
+ if (newSubAccount[name] !== duplicatedAccount[name]) {
400
+ // @ts-expect-error - TypeScript assumes all possible types could be assigned here
401
+ updates[name] = newSubAccount[name];
402
+ }
403
+ } else {
404
+ updates[name] = mergeOps(duplicatedAccount[name], newSubAccount[name]);
405
+ }
406
+ }
407
+
408
+ // update the operationsCount in case the mergeOps changed it
409
+ updates.operationsCount =
410
+ updates.operations?.length || duplicatedAccount?.operations?.length || 0;
411
+
412
+ // modify the map with the updated sub account with a new ref
413
+ oldSubAccountsById[newSubAccount.id!] = {
414
+ ...duplicatedAccount,
415
+ ...updates,
416
+ };
417
+ }
418
+
419
+ const updatedSubAccounts = Object.values(oldSubAccountsById);
420
+
421
+ return [...updatedSubAccounts, ...newSubAccountsToAdd];
422
+ };
423
+
424
+ export const applyPendingExtras = (existing: Operation[], pending: Operation[]) => {
425
+ const pendingOperationsByHash = new Map(pending.map(op => [op.hash, op]));
426
+
427
+ return existing.map(op => {
428
+ const pendingOp = pendingOperationsByHash.get(op.hash);
429
+ if (!pendingOp) return op;
430
+ if (!isValidExtra(op.extra)) return op;
431
+ if (!isValidExtra(pendingOp.extra)) return op;
432
+
433
+ return {
434
+ ...op,
435
+ extra: {
436
+ ...pendingOp.extra,
437
+ ...op.extra,
438
+ },
439
+ };
440
+ });
441
+ };
442
+
63
443
  export function patchOperationWithExtra(
64
444
  operation: Operation,
65
445
  extra: HederaOperationExtra,
@@ -71,3 +451,22 @@ export function patchOperationWithExtra(
71
451
  nftOperations: (operation.nftOperations ?? []).map(op => ({ ...op, extra })),
72
452
  };
73
453
  }
454
+
455
+ export const checkAccountTokenAssociationStatus = makeLRUCache(
456
+ async (accountId: string, tokenId: string) => {
457
+ const mirrorAccount = await getAccount(accountId);
458
+
459
+ // auto association is enabled
460
+ if (mirrorAccount.max_automatic_token_associations === -1) {
461
+ return true;
462
+ }
463
+
464
+ const isTokenAssociated = mirrorAccount.balance.tokens.some(token => {
465
+ return token.token_id === tokenId;
466
+ });
467
+
468
+ return isTokenAssociated;
469
+ },
470
+ (accountId, tokenId) => `${accountId}-${tokenId}`,
471
+ seconds(30),
472
+ );
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Internal types used to distinguish custom Hedera transaction behaviors.
3
+ * These can be stored in transaction.properties.name and used to route specific preparation logic.
4
+ */
5
+ export const HEDERA_TRANSACTION_KINDS = {
6
+ TokenAssociate: {
7
+ name: "tokenAssociate",
8
+ },
9
+ } as const satisfies Record<string, Record<string, unknown> & { name: string }>;
10
+
11
+ /**
12
+ * Enum representing the supported Hedera operation types for fee estimation
13
+ */
14
+ export enum HEDERA_OPERATION_TYPES {
15
+ CryptoTransfer = "CryptoTransfer",
16
+ TokenTransfer = "TokenTransfer",
17
+ TokenAssociate = "TokenAssociate",
18
+ }
19
+
20
+ const TINYBAR_SCALE = 8;
21
+
22
+ /**
23
+ * https://docs.hedera.com/hedera/networks/mainnet/fees
24
+ *
25
+ * These are Hedera's estimated fee costs in USD, scaled to tinybars (1 HBAR = 10^8 tinybars),
26
+ * so they can be converted into actual HBAR amounts based on current USD/crypto rates.
27
+ *
28
+ * Used in fee estimation logic (getEstimatedFees function) to determine whether an account
29
+ * has sufficient balance to cover the cost of a transaction (e.g. token association).
30
+ */
31
+ export const BASE_USD_FEE_BY_OPERATION_TYPE = {
32
+ [HEDERA_OPERATION_TYPES.CryptoTransfer]: 0.0001 * 10 ** TINYBAR_SCALE,
33
+ [HEDERA_OPERATION_TYPES.TokenTransfer]: 0.001 * 10 ** TINYBAR_SCALE,
34
+ [HEDERA_OPERATION_TYPES.TokenAssociate]: 0.05 * 10 ** TINYBAR_SCALE,
35
+ } as const satisfies Record<HEDERA_OPERATION_TYPES, number>;
@@ -1,6 +1,7 @@
1
1
  import type { AccountLike, Account } from "@ledgerhq/types-live";
2
2
  import type { Transaction, TransactionStatus } from "./types";
3
3
  import type { CommonDeviceTransactionField as DeviceTransactionField } from "@ledgerhq/coin-framework/transaction/common";
4
+ import { isTokenAssociateTransaction } from "./logic";
4
5
 
5
6
  function getDeviceTransactionConfig({
6
7
  transaction,
@@ -13,25 +14,25 @@ function getDeviceTransactionConfig({
13
14
  }): Array<DeviceTransactionField> {
14
15
  const fields: Array<DeviceTransactionField> = [];
15
16
 
16
- if (transaction.useAllAmount) {
17
- fields.push({
18
- type: "text",
19
- label: "Method",
20
- value: "Transfer All",
21
- });
22
- } else {
23
- fields.push({
24
- type: "text",
25
- label: "Method",
26
- value: "Transfer",
27
- });
28
- }
17
+ const method = (() => {
18
+ if (isTokenAssociateTransaction(transaction)) return "Associate Token";
19
+ else if (transaction.useAllAmount) return "Transfer All";
20
+ else return "Transfer";
21
+ })();
29
22
 
30
23
  fields.push({
31
- type: "amount",
32
- label: "Amount",
24
+ type: "text",
25
+ label: "Method",
26
+ value: method,
33
27
  });
34
28
 
29
+ if (!isTokenAssociateTransaction(transaction)) {
30
+ fields.push({
31
+ type: "amount",
32
+ label: "Amount",
33
+ });
34
+ }
35
+
35
36
  if (!estimatedFees.isZero()) {
36
37
  fields.push({
37
38
  type: "fees",