@ledgerhq/coin-hedera 1.15.0-nightly.20251126023856 → 1.15.0-nightly.20251126160702

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 (280) hide show
  1. package/CHANGELOG.md +10 -8
  2. package/lib/api/index.d.ts.map +1 -1
  3. package/lib/api/index.js +6 -2
  4. package/lib/api/index.js.map +1 -1
  5. package/lib/bridge/broadcast.js +1 -1
  6. package/lib/bridge/broadcast.js.map +1 -1
  7. package/lib/bridge/buildOptimisticOperation.d.ts.map +1 -1
  8. package/lib/bridge/buildOptimisticOperation.js +80 -15
  9. package/lib/bridge/buildOptimisticOperation.js.map +1 -1
  10. package/lib/bridge/estimateMaxSpendable.js +5 -2
  11. package/lib/bridge/estimateMaxSpendable.js.map +1 -1
  12. package/lib/bridge/getTransactionStatus.d.ts.map +1 -1
  13. package/lib/bridge/getTransactionStatus.js +70 -13
  14. package/lib/bridge/getTransactionStatus.js.map +1 -1
  15. package/lib/bridge/index.js +1 -1
  16. package/lib/bridge/index.js.map +1 -1
  17. package/lib/bridge/prepareTransaction.d.ts.map +1 -1
  18. package/lib/bridge/prepareTransaction.js +40 -7
  19. package/lib/bridge/prepareTransaction.js.map +1 -1
  20. package/lib/bridge/signOperation.d.ts.map +1 -1
  21. package/lib/bridge/signOperation.js +19 -2
  22. package/lib/bridge/signOperation.js.map +1 -1
  23. package/lib/bridge/synchronisation.d.ts +2 -0
  24. package/lib/bridge/synchronisation.d.ts.map +1 -1
  25. package/lib/bridge/synchronisation.js +101 -30
  26. package/lib/bridge/synchronisation.js.map +1 -1
  27. package/lib/bridge/utils.d.ts +35 -2
  28. package/lib/bridge/utils.d.ts.map +1 -1
  29. package/lib/bridge/utils.js +215 -16
  30. package/lib/bridge/utils.js.map +1 -1
  31. package/lib/constants.d.ts +22 -2
  32. package/lib/constants.d.ts.map +1 -1
  33. package/lib/constants.js +42 -2
  34. package/lib/constants.js.map +1 -1
  35. package/lib/deviceTransactionConfig.d.ts +1 -1
  36. package/lib/deviceTransactionConfig.d.ts.map +1 -1
  37. package/lib/deviceTransactionConfig.js +8 -0
  38. package/lib/deviceTransactionConfig.js.map +1 -1
  39. package/lib/errors.d.ts +3 -0
  40. package/lib/errors.d.ts.map +1 -1
  41. package/lib/errors.js +2 -1
  42. package/lib/errors.js.map +1 -1
  43. package/lib/logic/craftTransaction.d.ts +4 -4
  44. package/lib/logic/craftTransaction.d.ts.map +1 -1
  45. package/lib/logic/craftTransaction.js +46 -5
  46. package/lib/logic/craftTransaction.js.map +1 -1
  47. package/lib/logic/estimateFees.d.ts +2 -4
  48. package/lib/logic/estimateFees.d.ts.map +1 -1
  49. package/lib/logic/estimateFees.js +45 -5
  50. package/lib/logic/estimateFees.js.map +1 -1
  51. package/lib/logic/listOperations.d.ts.map +1 -1
  52. package/lib/logic/listOperations.js +7 -3
  53. package/lib/logic/listOperations.js.map +1 -1
  54. package/lib/logic/utils.d.ts +24 -2
  55. package/lib/logic/utils.d.ts.map +1 -1
  56. package/lib/logic/utils.js +66 -13
  57. package/lib/logic/utils.js.map +1 -1
  58. package/lib/network/api.d.ts +12 -1
  59. package/lib/network/api.d.ts.map +1 -1
  60. package/lib/network/api.js +91 -19
  61. package/lib/network/api.js.map +1 -1
  62. package/lib/network/rpc.d.ts.map +1 -1
  63. package/lib/network/rpc.js +1 -0
  64. package/lib/network/rpc.js.map +1 -1
  65. package/lib/network/thirdweb.d.ts +21 -0
  66. package/lib/network/thirdweb.d.ts.map +1 -0
  67. package/lib/network/thirdweb.js +72 -0
  68. package/lib/network/thirdweb.js.map +1 -0
  69. package/lib/network/utils.d.ts +4 -1
  70. package/lib/network/utils.d.ts.map +1 -1
  71. package/lib/network/utils.js +53 -1
  72. package/lib/network/utils.js.map +1 -1
  73. package/lib/test/bridgeDatasetTest.d.ts.map +1 -1
  74. package/lib/test/bridgeDatasetTest.js +4 -4
  75. package/lib/test/bridgeDatasetTest.js.map +1 -1
  76. package/lib/test/fixtures/account.fixture.js +1 -1
  77. package/lib/test/fixtures/account.fixture.js.map +1 -1
  78. package/lib/test/fixtures/common.fixture.d.ts +12 -0
  79. package/lib/test/fixtures/common.fixture.d.ts.map +1 -0
  80. package/lib/test/fixtures/common.fixture.js +66 -0
  81. package/lib/test/fixtures/common.fixture.js.map +1 -0
  82. package/lib/test/fixtures/currency.fixture.d.ts +3 -1
  83. package/lib/test/fixtures/currency.fixture.d.ts.map +1 -1
  84. package/lib/test/fixtures/currency.fixture.js +63 -16
  85. package/lib/test/fixtures/currency.fixture.js.map +1 -1
  86. package/lib/test/fixtures/mirror.fixture.d.ts +3 -1
  87. package/lib/test/fixtures/mirror.fixture.d.ts.map +1 -1
  88. package/lib/test/fixtures/mirror.fixture.js +12 -1
  89. package/lib/test/fixtures/mirror.fixture.js.map +1 -1
  90. package/lib/test/fixtures/thirdweb.fixture.d.ts +3 -0
  91. package/lib/test/fixtures/thirdweb.fixture.d.ts.map +1 -0
  92. package/lib/test/fixtures/thirdweb.fixture.js +34 -0
  93. package/lib/test/fixtures/thirdweb.fixture.js.map +1 -0
  94. package/lib/transaction.d.ts.map +1 -1
  95. package/lib/transaction.js +2 -0
  96. package/lib/transaction.js.map +1 -1
  97. package/lib/types/alpaca.d.ts +5 -1
  98. package/lib/types/alpaca.d.ts.map +1 -1
  99. package/lib/types/bridge.d.ts +6 -1
  100. package/lib/types/bridge.d.ts.map +1 -1
  101. package/lib/types/index.d.ts +2 -0
  102. package/lib/types/index.d.ts.map +1 -1
  103. package/lib/types/index.js +2 -0
  104. package/lib/types/index.js.map +1 -1
  105. package/lib/types/logic.d.ts +39 -0
  106. package/lib/types/logic.d.ts.map +1 -0
  107. package/lib/types/logic.js +3 -0
  108. package/lib/types/logic.js.map +1 -0
  109. package/lib/types/mirror.d.ts +29 -0
  110. package/lib/types/mirror.d.ts.map +1 -1
  111. package/lib/types/thirdweb.d.ts +34 -0
  112. package/lib/types/thirdweb.d.ts.map +1 -0
  113. package/lib/types/thirdweb.js +3 -0
  114. package/lib/types/thirdweb.js.map +1 -0
  115. package/lib-es/api/index.d.ts.map +1 -1
  116. package/lib-es/api/index.js +7 -3
  117. package/lib-es/api/index.js.map +1 -1
  118. package/lib-es/bridge/broadcast.js +2 -2
  119. package/lib-es/bridge/broadcast.js.map +1 -1
  120. package/lib-es/bridge/buildOptimisticOperation.d.ts.map +1 -1
  121. package/lib-es/bridge/buildOptimisticOperation.js +83 -18
  122. package/lib-es/bridge/buildOptimisticOperation.js.map +1 -1
  123. package/lib-es/bridge/estimateMaxSpendable.js +5 -2
  124. package/lib-es/bridge/estimateMaxSpendable.js.map +1 -1
  125. package/lib-es/bridge/getTransactionStatus.d.ts.map +1 -1
  126. package/lib-es/bridge/getTransactionStatus.js +73 -16
  127. package/lib-es/bridge/getTransactionStatus.js.map +1 -1
  128. package/lib-es/bridge/index.js +2 -2
  129. package/lib-es/bridge/index.js.map +1 -1
  130. package/lib-es/bridge/prepareTransaction.d.ts.map +1 -1
  131. package/lib-es/bridge/prepareTransaction.js +42 -9
  132. package/lib-es/bridge/prepareTransaction.js.map +1 -1
  133. package/lib-es/bridge/signOperation.d.ts.map +1 -1
  134. package/lib-es/bridge/signOperation.js +21 -4
  135. package/lib-es/bridge/signOperation.js.map +1 -1
  136. package/lib-es/bridge/synchronisation.d.ts +2 -0
  137. package/lib-es/bridge/synchronisation.d.ts.map +1 -1
  138. package/lib-es/bridge/synchronisation.js +97 -27
  139. package/lib-es/bridge/synchronisation.js.map +1 -1
  140. package/lib-es/bridge/utils.d.ts +35 -2
  141. package/lib-es/bridge/utils.d.ts.map +1 -1
  142. package/lib-es/bridge/utils.js +211 -16
  143. package/lib-es/bridge/utils.js.map +1 -1
  144. package/lib-es/constants.d.ts +22 -2
  145. package/lib-es/constants.d.ts.map +1 -1
  146. package/lib-es/constants.js +38 -1
  147. package/lib-es/constants.js.map +1 -1
  148. package/lib-es/deviceTransactionConfig.d.ts +1 -1
  149. package/lib-es/deviceTransactionConfig.d.ts.map +1 -1
  150. package/lib-es/deviceTransactionConfig.js +8 -0
  151. package/lib-es/deviceTransactionConfig.js.map +1 -1
  152. package/lib-es/errors.d.ts +3 -0
  153. package/lib-es/errors.d.ts.map +1 -1
  154. package/lib-es/errors.js +1 -0
  155. package/lib-es/errors.js.map +1 -1
  156. package/lib-es/logic/craftTransaction.d.ts +4 -4
  157. package/lib-es/logic/craftTransaction.d.ts.map +1 -1
  158. package/lib-es/logic/craftTransaction.js +48 -7
  159. package/lib-es/logic/craftTransaction.js.map +1 -1
  160. package/lib-es/logic/estimateFees.d.ts +2 -4
  161. package/lib-es/logic/estimateFees.d.ts.map +1 -1
  162. package/lib-es/logic/estimateFees.js +47 -7
  163. package/lib-es/logic/estimateFees.js.map +1 -1
  164. package/lib-es/logic/listOperations.d.ts.map +1 -1
  165. package/lib-es/logic/listOperations.js +7 -3
  166. package/lib-es/logic/listOperations.js.map +1 -1
  167. package/lib-es/logic/utils.d.ts +24 -2
  168. package/lib-es/logic/utils.d.ts.map +1 -1
  169. package/lib-es/logic/utils.js +63 -13
  170. package/lib-es/logic/utils.js.map +1 -1
  171. package/lib-es/network/api.d.ts +12 -1
  172. package/lib-es/network/api.d.ts.map +1 -1
  173. package/lib-es/network/api.js +91 -19
  174. package/lib-es/network/api.js.map +1 -1
  175. package/lib-es/network/rpc.d.ts.map +1 -1
  176. package/lib-es/network/rpc.js +1 -0
  177. package/lib-es/network/rpc.js.map +1 -1
  178. package/lib-es/network/thirdweb.d.ts +21 -0
  179. package/lib-es/network/thirdweb.d.ts.map +1 -0
  180. package/lib-es/network/thirdweb.js +66 -0
  181. package/lib-es/network/thirdweb.js.map +1 -0
  182. package/lib-es/network/utils.d.ts +4 -1
  183. package/lib-es/network/utils.d.ts.map +1 -1
  184. package/lib-es/network/utils.js +49 -0
  185. package/lib-es/network/utils.js.map +1 -1
  186. package/lib-es/test/bridgeDatasetTest.d.ts.map +1 -1
  187. package/lib-es/test/bridgeDatasetTest.js +4 -4
  188. package/lib-es/test/bridgeDatasetTest.js.map +1 -1
  189. package/lib-es/test/fixtures/account.fixture.js +2 -2
  190. package/lib-es/test/fixtures/account.fixture.js.map +1 -1
  191. package/lib-es/test/fixtures/common.fixture.d.ts +12 -0
  192. package/lib-es/test/fixtures/common.fixture.d.ts.map +1 -0
  193. package/lib-es/test/fixtures/common.fixture.js +57 -0
  194. package/lib-es/test/fixtures/common.fixture.js.map +1 -0
  195. package/lib-es/test/fixtures/currency.fixture.d.ts +3 -1
  196. package/lib-es/test/fixtures/currency.fixture.d.ts.map +1 -1
  197. package/lib-es/test/fixtures/currency.fixture.js +59 -14
  198. package/lib-es/test/fixtures/currency.fixture.js.map +1 -1
  199. package/lib-es/test/fixtures/mirror.fixture.d.ts +3 -1
  200. package/lib-es/test/fixtures/mirror.fixture.d.ts.map +1 -1
  201. package/lib-es/test/fixtures/mirror.fixture.js +9 -0
  202. package/lib-es/test/fixtures/mirror.fixture.js.map +1 -1
  203. package/lib-es/test/fixtures/thirdweb.fixture.d.ts +3 -0
  204. package/lib-es/test/fixtures/thirdweb.fixture.d.ts.map +1 -0
  205. package/lib-es/test/fixtures/thirdweb.fixture.js +30 -0
  206. package/lib-es/test/fixtures/thirdweb.fixture.js.map +1 -0
  207. package/lib-es/transaction.d.ts.map +1 -1
  208. package/lib-es/transaction.js +2 -0
  209. package/lib-es/transaction.js.map +1 -1
  210. package/lib-es/types/alpaca.d.ts +5 -1
  211. package/lib-es/types/alpaca.d.ts.map +1 -1
  212. package/lib-es/types/bridge.d.ts +6 -1
  213. package/lib-es/types/bridge.d.ts.map +1 -1
  214. package/lib-es/types/index.d.ts +2 -0
  215. package/lib-es/types/index.d.ts.map +1 -1
  216. package/lib-es/types/index.js +2 -0
  217. package/lib-es/types/index.js.map +1 -1
  218. package/lib-es/types/logic.d.ts +39 -0
  219. package/lib-es/types/logic.d.ts.map +1 -0
  220. package/lib-es/types/logic.js +2 -0
  221. package/lib-es/types/logic.js.map +1 -0
  222. package/lib-es/types/mirror.d.ts +29 -0
  223. package/lib-es/types/mirror.d.ts.map +1 -1
  224. package/lib-es/types/thirdweb.d.ts +34 -0
  225. package/lib-es/types/thirdweb.d.ts.map +1 -0
  226. package/lib-es/types/thirdweb.js +2 -0
  227. package/lib-es/types/thirdweb.js.map +1 -0
  228. package/package.json +9 -8
  229. package/src/api/index.integ.test.ts +11 -8
  230. package/src/api/index.ts +10 -3
  231. package/src/bridge/broadcast.ts +2 -2
  232. package/src/bridge/buildOptimisticOperation.integration.test.ts +70 -19
  233. package/src/bridge/buildOptimisticOperation.ts +98 -20
  234. package/src/bridge/estimateMaxSpendable.ts +5 -5
  235. package/src/bridge/getTransactionStatus.test.ts +57 -12
  236. package/src/bridge/getTransactionStatus.ts +88 -15
  237. package/src/bridge/index.ts +2 -2
  238. package/src/bridge/js-estimateMaxSpendable.integration.test.ts +12 -9
  239. package/src/bridge/prepareTransaction.test.ts +3 -1
  240. package/src/bridge/prepareTransaction.ts +45 -10
  241. package/src/bridge/signOperation.ts +23 -5
  242. package/src/bridge/synchronisation.test.ts +67 -0
  243. package/src/bridge/synchronisation.ts +114 -34
  244. package/src/bridge/utils.integration.test.ts +486 -180
  245. package/src/bridge/utils.test.ts +404 -0
  246. package/src/bridge/utils.ts +330 -27
  247. package/src/constants.ts +47 -2
  248. package/src/deviceTransactionConfig.ts +10 -1
  249. package/src/errors.ts +3 -0
  250. package/src/logic/craftTransaction.test.ts +49 -9
  251. package/src/logic/craftTransaction.ts +76 -11
  252. package/src/logic/estimateFees.test.ts +180 -31
  253. package/src/logic/estimateFees.ts +68 -7
  254. package/src/logic/getAssetFromToken.test.ts +2 -2
  255. package/src/logic/getBalance.test.ts +18 -57
  256. package/src/logic/getTokenFromAsset.test.ts +2 -2
  257. package/src/logic/listOperations.ts +9 -5
  258. package/src/logic/utils.test.ts +157 -69
  259. package/src/logic/utils.ts +75 -13
  260. package/src/network/api.test.ts +211 -3
  261. package/src/network/api.ts +118 -24
  262. package/src/network/rpc.test.ts +1 -0
  263. package/src/network/rpc.ts +1 -0
  264. package/src/network/thirdweb.test.ts +188 -0
  265. package/src/network/thirdweb.ts +101 -0
  266. package/src/network/utils.test.ts +364 -164
  267. package/src/network/utils.ts +83 -1
  268. package/src/test/bridgeDatasetTest.ts +4 -5
  269. package/src/test/fixtures/account.fixture.ts +2 -2
  270. package/src/test/fixtures/common.fixture.ts +74 -0
  271. package/src/test/fixtures/currency.fixture.ts +66 -14
  272. package/src/test/fixtures/mirror.fixture.ts +23 -1
  273. package/src/test/fixtures/thirdweb.fixture.ts +33 -0
  274. package/src/transaction.ts +2 -0
  275. package/src/types/alpaca.ts +8 -1
  276. package/src/types/bridge.ts +6 -1
  277. package/src/types/index.ts +2 -0
  278. package/src/types/logic.ts +44 -0
  279. package/src/types/mirror.ts +35 -0
  280. package/src/types/thirdweb.ts +36 -0
@@ -1,5 +1,5 @@
1
1
  import BigNumber from "bignumber.js";
2
- import type { Account, Operation, TokenAccount } from "@ledgerhq/types-live";
2
+ import type { Account, Operation, OperationType, TokenAccount } from "@ledgerhq/types-live";
3
3
  import {
4
4
  decodeTokenAccountId,
5
5
  emptyHistoryCache,
@@ -7,15 +7,31 @@ import {
7
7
  findSubAccountById,
8
8
  isTokenAccount,
9
9
  } from "@ledgerhq/coin-framework/account";
10
+ import { mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers";
10
11
  import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
11
12
  import type { TokenCurrency } from "@ledgerhq/types-cryptoassets";
12
- import { mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers";
13
13
  import { getCryptoAssetsStore } from "@ledgerhq/cryptoassets/state";
14
14
  import { HEDERA_OPERATION_TYPES } from "../constants";
15
15
  import { estimateMaxSpendable } from "./estimateMaxSpendable";
16
16
  import { estimateFees } from "../logic/estimateFees";
17
- import { isTokenAssociateTransaction, isValidExtra } from "../logic/utils";
18
- import type { HederaOperationExtra, Transaction, HederaMirrorToken } from "../types";
17
+ import {
18
+ fromEVMAddress,
19
+ toEVMAddress,
20
+ getMemoFromBase64,
21
+ isTokenAssociateTransaction,
22
+ isValidExtra,
23
+ base64ToUrlSafeBase64,
24
+ } from "../logic/utils";
25
+ import { getERC20Operations, parseThirdwebTransactionParams } from "../network/utils";
26
+ import type {
27
+ HederaOperationExtra,
28
+ Transaction,
29
+ OperationERC20,
30
+ HederaMirrorToken,
31
+ HederaERC20TokenBalance,
32
+ HederaThirdwebTransaction,
33
+ ERC20OperationFields,
34
+ } from "../types";
19
35
 
20
36
  interface CalculateAmountResult {
21
37
  amount: BigNumber;
@@ -29,16 +45,16 @@ const calculateCoinAmount = async ({
29
45
  }: {
30
46
  account: Account;
31
47
  transaction: Transaction;
32
- operationType: HEDERA_OPERATION_TYPES;
48
+ operationType: Exclude<HEDERA_OPERATION_TYPES, HEDERA_OPERATION_TYPES.ContractCall>;
33
49
  }): Promise<CalculateAmountResult> => {
34
- const estimatedFees = await estimateFees(account.currency, operationType);
50
+ const estimatedFees = await estimateFees({ currency: account.currency, operationType });
35
51
  const amount = transaction.useAllAmount
36
52
  ? await estimateMaxSpendable({ account, transaction })
37
53
  : transaction.amount;
38
54
 
39
55
  return {
40
56
  amount,
41
- totalSpent: amount.plus(estimatedFees),
57
+ totalSpent: amount.plus(estimatedFees.tinybars),
42
58
  };
43
59
  };
44
60
 
@@ -82,15 +98,24 @@ export const calculateAmount = ({
82
98
  return calculateCoinAmount({ account, transaction, operationType });
83
99
  };
84
100
 
85
- export const getSubAccounts = async (
86
- accountId: string,
87
- lastTokenOperations: Operation[],
88
- mirrorTokens: HederaMirrorToken[],
89
- ): Promise<TokenAccount[]> => {
101
+ export const getSubAccounts = async ({
102
+ ledgerAccountId,
103
+ latestHTSTokenOperations,
104
+ latestERC20TokenOperations,
105
+ mirrorTokens,
106
+ erc20Tokens,
107
+ }: {
108
+ ledgerAccountId: string;
109
+ latestHTSTokenOperations: Operation[];
110
+ latestERC20TokenOperations: Operation[];
111
+ mirrorTokens: HederaMirrorToken[];
112
+ erc20Tokens: HederaERC20TokenBalance[];
113
+ }): Promise<TokenAccount[]> => {
90
114
  // Creating a Map of Operations by TokenCurrencies in order to know which TokenAccounts should be synced as well
91
115
  const operationsByToken = new Map<TokenCurrency, Operation[]>();
116
+ const subAccounts: TokenAccount[] = [];
92
117
 
93
- for (const tokenOperation of lastTokenOperations) {
118
+ for (const tokenOperation of [...latestHTSTokenOperations, ...latestERC20TokenOperations]) {
94
119
  const { token } = await decodeTokenAccountId(tokenOperation.accountId);
95
120
  if (!token) continue;
96
121
 
@@ -107,13 +132,18 @@ export const getSubAccounts = async (
107
132
  operationsByToken.get(token)?.push(tokenOperation);
108
133
  }
109
134
 
110
- const subAccounts: TokenAccount[] = [];
111
-
112
135
  // extract token accounts from existing operations
113
136
  for (const [token, tokenOperations] of operationsByToken.entries()) {
114
- const parentAccountId = accountId;
115
- const rawBalance = mirrorTokens.find(t => t.token_id === token.contractAddress)?.balance;
116
- const balance = rawBalance !== undefined ? new BigNumber(rawBalance) : null;
137
+ const parentAccountId = ledgerAccountId;
138
+ let balance: BigNumber | null = null;
139
+
140
+ if (token.tokenType === "erc20") {
141
+ const rawBalance = erc20Tokens.find(t => t.token.contractAddress === token.contractAddress);
142
+ balance = rawBalance === undefined ? null : new BigNumber(rawBalance.balance);
143
+ } else {
144
+ const rawBalance = mirrorTokens.find(t => t.token_id === token.contractAddress)?.balance;
145
+ balance = rawBalance === undefined ? null : new BigNumber(rawBalance);
146
+ }
117
147
 
118
148
  if (!balance) {
119
149
  continue;
@@ -137,34 +167,41 @@ export const getSubAccounts = async (
137
167
  }
138
168
 
139
169
  // extract token accounts existing in the account's balance, but with no recorded operations yet
140
- // e.g. tokens added via association flow, without any subsequent activity
141
- for (const rawToken of mirrorTokens) {
142
- const parentAccountId = accountId;
170
+ // e.g. hts tokens added via association flow, without any subsequent activity
171
+ // or erc20 tokens received from 3rd party
172
+ for (const rawToken of [...mirrorTokens, ...erc20Tokens]) {
173
+ const parentAccountId = ledgerAccountId;
143
174
  const rawBalance = rawToken.balance;
144
175
  const balance = new BigNumber(rawBalance);
145
- const token = await getCryptoAssetsStore().findTokenByAddressInCurrency(
146
- rawToken.token_id,
147
- "hedera",
148
- );
176
+ const isERC20 = "token" in rawToken;
177
+ const tokenAddress = isERC20 ? rawToken.token.contractAddress : rawToken.token_id;
178
+ const token = await getCryptoAssetsStore().findTokenByAddressInCurrency(tokenAddress, "hedera");
149
179
 
150
180
  if (!token) {
151
181
  continue;
152
182
  }
153
183
 
154
184
  const id = encodeTokenAccountId(parentAccountId, token);
185
+ const operations = operationsByToken.get(token) ?? [];
155
186
 
156
187
  if (subAccounts.some(a => a.id === id)) {
157
188
  continue;
158
189
  }
159
190
 
191
+ if (isERC20 && operations.length === 0 && balance.isZero()) {
192
+ continue;
193
+ }
194
+
160
195
  subAccounts.push({
161
196
  type: "TokenAccount",
162
- id: encodeTokenAccountId(parentAccountId, token),
197
+ id,
163
198
  parentId: parentAccountId,
164
199
  token,
165
200
  balance,
166
201
  spendableBalance: balance,
167
- creationDate: new Date(parseFloat(rawToken.created_timestamp) * 1000),
202
+ creationDate: isERC20
203
+ ? new Date()
204
+ : new Date(Number.parseFloat(rawToken.created_timestamp) * 1000),
168
205
  operations: [],
169
206
  operationsCount: 0,
170
207
  pendingOperations: [],
@@ -350,3 +387,269 @@ export function patchOperationWithExtra(
350
387
  nftOperations: (operation.nftOperations ?? []).map(op => ({ ...op, extra })),
351
388
  };
352
389
  }
390
+
391
+ // filter out CONTRACT_CALL operations based on pending and already existing ERC20 operations to avoid duplicates
392
+ export const removeDuplicatedContractCallOperations = (
393
+ operations: Operation[],
394
+ pendingOperationHashes: Set<string>,
395
+ erc20OperationHashes: Set<string>,
396
+ ): Operation[] => {
397
+ return operations.filter(op => {
398
+ if (op.type !== "CONTRACT_CALL") {
399
+ return true;
400
+ }
401
+
402
+ const hashAlreadyExists =
403
+ erc20OperationHashes.has(op.hash) || pendingOperationHashes.has(op.hash);
404
+
405
+ return !hashAlreadyExists;
406
+ });
407
+ };
408
+
409
+ // loop over latestERC20Operations and prepare lists of transactions that should be patched and added
410
+ // - patching happens when we have a matching CONTRACT_CALL operation without blockHash set (mirror node transaction without ERC20 details)
411
+ // - adding happens when we have no matching operation
412
+ export const classifyERC20Operations = ({
413
+ latestERC20Operations,
414
+ operationsByHash,
415
+ evmAccountAddress,
416
+ }: {
417
+ latestERC20Operations: OperationERC20[];
418
+ operationsByHash: Map<string, Operation>;
419
+ evmAccountAddress: string | null;
420
+ }): {
421
+ erc20OperationsToPatch: Map<string, OperationERC20>;
422
+ erc20OperationsToAdd: Map<string, OperationERC20>;
423
+ } => {
424
+ const erc20OperationsToPatch = new Map<string, OperationERC20>();
425
+ const erc20OperationsToAdd = new Map<string, OperationERC20>();
426
+
427
+ for (const erc20Operation of latestERC20Operations) {
428
+ const hash = base64ToUrlSafeBase64(erc20Operation.mirrorTransaction.transaction_hash);
429
+ const existingOp = operationsByHash.get(hash);
430
+ const type =
431
+ erc20Operation.thirdwebTransaction.decoded.params.from === evmAccountAddress ? "OUT" : "IN";
432
+
433
+ if (!existingOp) {
434
+ erc20OperationsToAdd.set(hash, erc20Operation);
435
+ continue;
436
+ }
437
+
438
+ if (existingOp.type === "CONTRACT_CALL" && type === "OUT" && !existingOp.blockHash) {
439
+ erc20OperationsToPatch.set(hash, erc20Operation);
440
+ continue;
441
+ }
442
+ }
443
+
444
+ return { erc20OperationsToPatch, erc20OperationsToAdd };
445
+ };
446
+
447
+ // extracts common fields from an ERC20 operation
448
+ const buildERC20OperationFields = ({
449
+ erc20Operation,
450
+ relatedExistingOperation,
451
+ variant,
452
+ evmAddress,
453
+ }: {
454
+ variant: "patch" | "add";
455
+ evmAddress: string | null;
456
+ erc20Operation: OperationERC20;
457
+ relatedExistingOperation?: Operation;
458
+ }): ERC20OperationFields | null => {
459
+ const decodedParams = parseThirdwebTransactionParams(erc20Operation.thirdwebTransaction);
460
+
461
+ if (!decodedParams) {
462
+ return null;
463
+ }
464
+
465
+ let type: OperationType = "OUT";
466
+ const standard = "erc20";
467
+ const blockHeight = 5;
468
+ const blockHash = erc20Operation.thirdwebTransaction.blockHash;
469
+ const consensusTimestamp = erc20Operation.mirrorTransaction.consensus_timestamp;
470
+ const timestamp = new Date(Number.parseInt(consensusTimestamp.split(".")[0], 10) * 1000);
471
+ const fee = BigNumber(erc20Operation.mirrorTransaction.charged_tx_fee);
472
+ const value = BigNumber(decodedParams.value);
473
+ const senderAddress = fromEVMAddress(decodedParams.from) ?? decodedParams.from;
474
+ const recipientAddress = fromEVMAddress(decodedParams.to) ?? decodedParams.to;
475
+ const memo = getMemoFromBase64(erc20Operation.mirrorTransaction.memo_base64);
476
+ const extra: HederaOperationExtra = {
477
+ ...(isValidExtra(relatedExistingOperation?.extra) && relatedExistingOperation.extra),
478
+ ...(memo && { memo }),
479
+ consensusTimestamp: erc20Operation.contractCallResult.timestamp,
480
+ transactionId: erc20Operation.mirrorTransaction.transaction_id,
481
+ gasConsumed: erc20Operation.contractCallResult.gas_consumed,
482
+ gasLimit: erc20Operation.contractCallResult.gas_limit,
483
+ gasUsed: erc20Operation.contractCallResult.gas_used,
484
+ };
485
+
486
+ if (variant === "add") {
487
+ type = decodedParams.from === evmAddress ? "OUT" : "IN";
488
+ }
489
+
490
+ return {
491
+ date: timestamp,
492
+ type,
493
+ fee,
494
+ value,
495
+ senders: [senderAddress],
496
+ recipients: [recipientAddress],
497
+ blockHeight,
498
+ blockHash,
499
+ extra,
500
+ standard,
501
+ hasFailed: false,
502
+ };
503
+ };
504
+
505
+ // patches an existing CONTRACT_CALL operation with ERC20 token operation details
506
+ export const patchContractCallOperation = ({
507
+ relatedExistingOperation,
508
+ ledgerAccountId,
509
+ hash,
510
+ erc20Fields,
511
+ tokenOperation,
512
+ }: {
513
+ relatedExistingOperation: Operation;
514
+ ledgerAccountId: string;
515
+ hash: string;
516
+ erc20Fields: ERC20OperationFields;
517
+ tokenOperation: Operation;
518
+ }): void => {
519
+ Object.assign(relatedExistingOperation, {
520
+ ...erc20Fields,
521
+ id: encodeOperationId(ledgerAccountId, hash, "FEES"),
522
+ type: "FEES",
523
+ value: erc20Fields.fee,
524
+ subOperations: [tokenOperation],
525
+ });
526
+ };
527
+
528
+ export const integrateERC20Operations = async ({
529
+ ledgerAccountId,
530
+ address,
531
+ allOperations,
532
+ latestERC20Transactions,
533
+ pendingOperationHashes,
534
+ erc20OperationHashes,
535
+ }: {
536
+ ledgerAccountId: string;
537
+ address: string;
538
+ allOperations: Operation[];
539
+ latestERC20Transactions: HederaThirdwebTransaction[];
540
+ pendingOperationHashes: Set<string>;
541
+ erc20OperationHashes: Set<string>;
542
+ }): Promise<{
543
+ updatedOperations: Operation[];
544
+ newERC20TokenOperations: Operation[];
545
+ }> => {
546
+ const newERC20TokenOperations: Operation[] = [];
547
+ const latestERC20Operations = await getERC20Operations(latestERC20Transactions);
548
+ const evmAddress = toEVMAddress(address);
549
+
550
+ // avoid duplicated CONTRACT_CALL operations if ERC20 operations are already present
551
+ const uniqueOperations = removeDuplicatedContractCallOperations(
552
+ allOperations,
553
+ pendingOperationHashes,
554
+ erc20OperationHashes,
555
+ );
556
+
557
+ // nothing to patch/add if no new ERC20 operations found
558
+ if (latestERC20Operations.length === 0) {
559
+ return {
560
+ updatedOperations: uniqueOperations,
561
+ newERC20TokenOperations,
562
+ };
563
+ }
564
+
565
+ // create copy to avoid mutating original array and index by hash for easy lookup
566
+ const updatedOperations = uniqueOperations.map(op => ({ ...op }));
567
+ const operationsByHash = updatedOperations.reduce((acc, curr) => {
568
+ acc.set(curr.hash, curr);
569
+ return acc;
570
+ }, new Map<string, Operation>());
571
+
572
+ // split erc20 operations into patch and add lists
573
+ const { erc20OperationsToPatch, erc20OperationsToAdd } = classifyERC20Operations({
574
+ latestERC20Operations,
575
+ operationsByHash,
576
+ evmAccountAddress: evmAddress,
577
+ });
578
+
579
+ // patch existing operations with data from thirdweb
580
+ for (const [hash, erc20Operation] of erc20OperationsToPatch.entries()) {
581
+ const relatedExistingOperation = operationsByHash.get(hash);
582
+ if (!relatedExistingOperation) continue;
583
+
584
+ const erc20Fields = buildERC20OperationFields({
585
+ variant: "patch",
586
+ evmAddress,
587
+ erc20Operation,
588
+ relatedExistingOperation,
589
+ });
590
+ if (!erc20Fields) continue;
591
+
592
+ const encodedTokenAccountId = encodeTokenAccountId(ledgerAccountId, erc20Operation.token);
593
+ const encodedOperationId = encodeOperationId(encodedTokenAccountId, hash, erc20Fields.type);
594
+ const tokenOperation: Operation<HederaOperationExtra> = {
595
+ ...erc20Fields,
596
+ id: encodedOperationId,
597
+ accountId: encodedTokenAccountId,
598
+ hash,
599
+ };
600
+
601
+ patchContractCallOperation({
602
+ relatedExistingOperation,
603
+ ledgerAccountId,
604
+ hash,
605
+ erc20Fields,
606
+ tokenOperation,
607
+ });
608
+
609
+ newERC20TokenOperations.push(tokenOperation);
610
+ }
611
+
612
+ // create new operations for remaining ERC20 operations
613
+ for (const [hash, erc20Operation] of erc20OperationsToAdd.entries()) {
614
+ const erc20Fields = buildERC20OperationFields({
615
+ variant: "add",
616
+ evmAddress,
617
+ erc20Operation,
618
+ });
619
+ if (!erc20Fields) continue;
620
+
621
+ const encodedTokenAccountId = encodeTokenAccountId(ledgerAccountId, erc20Operation.token);
622
+ const encodedOperationId = encodeOperationId(encodedTokenAccountId, hash, erc20Fields.type);
623
+ const tokenOperation: Operation<HederaOperationExtra> = {
624
+ ...erc20Fields,
625
+ id: encodedOperationId,
626
+ accountId: encodedTokenAccountId,
627
+ hash,
628
+ };
629
+
630
+ const coinOperation: Operation<HederaOperationExtra> =
631
+ erc20Fields.type === "OUT"
632
+ ? {
633
+ ...erc20Fields,
634
+ id: encodeOperationId(ledgerAccountId, hash, "FEES"),
635
+ accountId: ledgerAccountId,
636
+ type: "FEES",
637
+ value: erc20Fields.fee,
638
+ hash,
639
+ }
640
+ : await makeCoinOperationForOrphanChildOperation(tokenOperation);
641
+
642
+ coinOperation.subOperations = [tokenOperation];
643
+ updatedOperations.push(coinOperation);
644
+ newERC20TokenOperations.push(tokenOperation);
645
+ }
646
+
647
+ // ensure operations lists are sorted correctly
648
+ updatedOperations.sort((a, b) => b.date.getTime() - a.date.getTime());
649
+ newERC20TokenOperations.sort((a, b) => b.date.getTime() - a.date.getTime());
650
+
651
+ return {
652
+ updatedOperations,
653
+ newERC20TokenOperations,
654
+ };
655
+ };
package/src/constants.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import BigNumber from "bignumber.js";
2
+
1
3
  /**
2
4
  * Internal types to distinguish custom Hedera transaction behaviors.
3
5
  * These can be used in transaction.mode and used to route specific preparation logic.
@@ -14,17 +16,29 @@ export enum HEDERA_OPERATION_TYPES {
14
16
  CryptoTransfer = "CryptoTransfer",
15
17
  TokenTransfer = "TokenTransfer",
16
18
  TokenAssociate = "TokenAssociate",
19
+ ContractCall = "ContractCall",
17
20
  }
18
21
 
19
22
  export const TINYBAR_SCALE = 8;
20
23
 
21
- export const ESTIMATED_FEE_SAFETY_RATE = 2;
22
-
23
24
  // old value moved from https://github.com/LedgerHQ/ledger-live/blob/8447b68b7c6f1e7ccd4aa9db4da0e6c8de36a88e/libs/coin-modules/coin-hedera/src/bridge/utils.ts#L77
24
25
  export const DEFAULT_TINYBAR_FEE = 150200;
25
26
 
26
27
  export const SYNTHETIC_BLOCK_WINDOW_SECONDS = 10;
27
28
 
29
+ export const ERC20_TRANSFER_EVENT_TOPIC =
30
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
31
+
32
+ export const ESTIMATED_GAS_SAFETY_RATE = 1.2;
33
+
34
+ export const ESTIMATED_FEE_SAFETY_RATE = 2;
35
+
36
+ export const DEFAULT_GAS_LIMIT = new BigNumber(100_000);
37
+
38
+ export const DEFAULT_GAS_PRICE_TINYBARS = new BigNumber(100);
39
+
40
+ export const HEDERA_MAINNET_CHAIN_ID = 295;
41
+
28
42
  /**
29
43
  * https://docs.hedera.com/hedera/networks/mainnet/fees
30
44
  *
@@ -38,4 +52,35 @@ export const BASE_USD_FEE_BY_OPERATION_TYPE = {
38
52
  [HEDERA_OPERATION_TYPES.CryptoTransfer]: 0.0001 * 10 ** TINYBAR_SCALE,
39
53
  [HEDERA_OPERATION_TYPES.TokenTransfer]: 0.001 * 10 ** TINYBAR_SCALE,
40
54
  [HEDERA_OPERATION_TYPES.TokenAssociate]: 0.05 * 10 ** TINYBAR_SCALE,
55
+ [HEDERA_OPERATION_TYPES.ContractCall]: 0, // Contract call fees are based on gas used and are handled separately
41
56
  } as const satisfies Record<HEDERA_OPERATION_TYPES, number>;
57
+
58
+ /**
59
+ * Array of supported ERC20 token IDs for Hedera.
60
+ *
61
+ * This is a temporary solution to allow bypassing deprecated methods.
62
+ * It is essential to update this list in the future to ensure support
63
+ * for other erc20 tokens.
64
+ */
65
+ export const SUPPORTED_ERC20_TOKENS = [
66
+ {
67
+ id: "hedera/erc20/weth_0xca367694cdac8f152e33683bb36cc9d6a73f1ef2",
68
+ contractAddress: "0xca367694cdac8f152e33683bb36cc9d6a73f1ef2",
69
+ tokenId: "0.0.9470869",
70
+ },
71
+ {
72
+ id: "hedera/erc20/bonzo_atoken_usdc_0xb7687538c7f4cad022d5e97cc778d0b46457c5db",
73
+ contractAddress: "0xb7687538c7f4cad022d5e97cc778d0b46457c5db",
74
+ tokenId: "0.0.7308496",
75
+ },
76
+ {
77
+ id: "hedera/erc20/audd_0x39ceba2b467fa987546000eb5d1373acf1f3a2e1",
78
+ contractAddress: "0x39ceba2b467fa987546000eb5d1373acf1f3a2e1",
79
+ tokenId: "0.0.8317070",
80
+ },
81
+ {
82
+ id: "hedera/erc20/wrapped_btc_0xd7d4d91d64a6061fa00a94e2b3a2d2a5fb677849",
83
+ contractAddress: "0xd7d4d91d64a6061fa00a94e2b3a2d2a5fb677849",
84
+ tokenId: "0.0.10047837",
85
+ },
86
+ ];
@@ -1,5 +1,6 @@
1
- import type { AccountLike, Account } from "@ledgerhq/types-live";
2
1
  import type { CommonDeviceTransactionField as DeviceTransactionField } from "@ledgerhq/coin-framework/transaction/common";
2
+ import type { AccountLike, Account } from "@ledgerhq/types-live";
3
+ import { HEDERA_TRANSACTION_MODES } from "./constants";
3
4
  import { isTokenAssociateTransaction } from "./logic/utils";
4
5
  import type { Transaction, TransactionStatus } from "./types";
5
6
 
@@ -40,6 +41,14 @@ async function getDeviceTransactionConfig({
40
41
  });
41
42
  }
42
43
 
44
+ if (transaction.mode === HEDERA_TRANSACTION_MODES.Send && transaction.gasLimit) {
45
+ fields.push({
46
+ type: "text",
47
+ label: "Gas Limit",
48
+ value: transaction.gasLimit.toString(),
49
+ });
50
+ }
51
+
43
52
  if (transaction.memo) {
44
53
  fields.push({
45
54
  type: "text",
package/src/errors.ts CHANGED
@@ -13,3 +13,6 @@ export const HederaRecipientTokenAssociationRequired = createCustomErrorClass(
13
13
  export const HederaRecipientTokenAssociationUnverified = createCustomErrorClass(
14
14
  "HederaRecipientTokenAssociationUnverified",
15
15
  );
16
+ export const HederaRecipientEvmAddressVerificationRequired = createCustomErrorClass(
17
+ "HederaRecipientEvmAddressVerificationRequired",
18
+ );
@@ -3,7 +3,7 @@ import * as sdk from "@hashgraph/sdk";
3
3
  import type { FeeEstimation, TransactionIntent } from "@ledgerhq/coin-framework/api/index";
4
4
  import { HEDERA_TRANSACTION_MODES, TINYBAR_SCALE } from "../constants";
5
5
  import { craftTransaction } from "./craftTransaction";
6
- import type { HederaMemo } from "../types";
6
+ import type { HederaMemo, HederaTxData } from "../types";
7
7
  import { serializeTransaction } from "./utils";
8
8
 
9
9
  jest.mock("./utils");
@@ -39,8 +39,8 @@ describe("craftTransaction", () => {
39
39
  const senderTransfer = result.tx.hbarTransfers?.get(txIntent.sender);
40
40
  const recipientTransfer = result.tx.hbarTransfers?.get(txIntent.recipient);
41
41
 
42
- expect(senderTransfer).toEqual(sdk.Hbar.fromTinybars(-txIntent.amount));
43
- expect(recipientTransfer).toEqual(sdk.Hbar.fromTinybars(txIntent.amount));
42
+ expect(senderTransfer).toEqual(sdk.Hbar.fromTinybars((-txIntent.amount).toString()));
43
+ expect(recipientTransfer).toEqual(sdk.Hbar.fromTinybars(txIntent.amount.toString()));
44
44
  expect(result.tx.transactionMemo).toBe(txIntent.memo.value);
45
45
  expect(serializeTransaction).toHaveBeenCalled();
46
46
  expect(result).toEqual({
@@ -49,7 +49,7 @@ describe("craftTransaction", () => {
49
49
  });
50
50
  });
51
51
 
52
- it("should craft a token transfer transaction", async () => {
52
+ it("should craft HTS token transfer transaction", async () => {
53
53
  const txIntent = {
54
54
  intentType: "transaction",
55
55
  type: HEDERA_TRANSACTION_MODES.Send,
@@ -76,10 +76,50 @@ describe("craftTransaction", () => {
76
76
  const senderTransfer = tokenTransfers?.get(txIntent.sender);
77
77
  const recipientTransfer = tokenTransfers?.get(txIntent.recipient);
78
78
 
79
- // .toString() is used because tokenTransfers values are Long objects
80
- // this is internal dependency of @hashgraph/sdk, re-exported in newer version
81
- expect(senderTransfer?.toString()).toEqual((-txIntent.amount).toString());
82
- expect(recipientTransfer?.toString()).toEqual(txIntent.amount.toString());
79
+ expect(senderTransfer).toEqual(sdk.Long.fromBigInt(-txIntent.amount));
80
+ expect(recipientTransfer).toEqual(sdk.Long.fromBigInt(txIntent.amount));
81
+ expect(result.tx.transactionMemo).toBe("Token transfer");
82
+ expect(serializeTransaction).toHaveBeenCalled();
83
+ expect(result).toEqual({
84
+ tx: expect.any(Object),
85
+ serializedTx: "serialized-transaction",
86
+ });
87
+ });
88
+
89
+ it("should craft ERC20 token transfer transaction", async () => {
90
+ const txIntent = {
91
+ intentType: "transaction",
92
+ type: HEDERA_TRANSACTION_MODES.Send,
93
+ amount: BigInt(1000),
94
+ recipient: "0.0.12345",
95
+ sender: "0.0.54321",
96
+ asset: {
97
+ type: "erc20",
98
+ assetReference: "0x39ceba2b467fa987546000eb5d1373acf1f3a2e1",
99
+ },
100
+ memo: {
101
+ kind: "text",
102
+ type: "string",
103
+ value: "Token transfer",
104
+ },
105
+ data: {
106
+ type: "erc20",
107
+ gasLimit: BigInt(100000),
108
+ },
109
+ } satisfies TransactionIntent<HederaMemo, HederaTxData>;
110
+
111
+ const result = await craftTransaction(txIntent);
112
+
113
+ expect(result.tx).toBeInstanceOf(sdk.ContractExecuteTransaction);
114
+ invariant(
115
+ result.tx instanceof sdk.ContractExecuteTransaction,
116
+ "ContractExecuteTransaction type guard",
117
+ );
118
+
119
+ expect(result.tx.gas).toEqual(sdk.Long.fromBigInt(txIntent.data.gasLimit));
120
+ expect(result.tx.contractId).toEqual(
121
+ sdk.ContractId.fromEvmAddress(0, 0, txIntent.asset.assetReference),
122
+ );
83
123
  expect(result.tx.transactionMemo).toBe("Token transfer");
84
124
  expect(serializeTransaction).toHaveBeenCalled();
85
125
  expect(result).toEqual({
@@ -150,7 +190,7 @@ describe("craftTransaction", () => {
150
190
 
151
191
  expect(result.tx).toBeInstanceOf(sdk.TransferTransaction);
152
192
  invariant(result.tx instanceof sdk.TransferTransaction, "TransferTransaction type guard");
153
- expect(result.tx.maxTransactionFee).toEqual(sdk.Hbar.fromTinybars(customFees.value));
193
+ expect(result.tx.maxTransactionFee).toEqual(sdk.Hbar.fromTinybars(customFees.value.toString()));
154
194
  });
155
195
 
156
196
  it("should throw error when token associate transaction has invalid asset type", async () => {