@ledgerhq/coin-hedera 1.10.1 → 1.11.0-nightly.2

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 (249) hide show
  1. package/.eslintrc.js +1 -0
  2. package/.turbo/turbo-build.log +1 -1
  3. package/CHANGELOG.md +37 -12
  4. package/lib/api/mirror.d.ts +3 -20
  5. package/lib/api/mirror.d.ts.map +1 -1
  6. package/lib/api/mirror.js +32 -90
  7. package/lib/api/mirror.js.map +1 -1
  8. package/lib/api/mirror.test.js +59 -4
  9. package/lib/api/mirror.test.js.map +1 -1
  10. package/lib/api/network.d.ts +3 -3
  11. package/lib/api/network.d.ts.map +1 -1
  12. package/lib/api/network.js +46 -3
  13. package/lib/api/network.js.map +1 -1
  14. package/lib/api/types.d.ts +44 -0
  15. package/lib/api/types.d.ts.map +1 -0
  16. package/lib/api/types.js +3 -0
  17. package/lib/api/types.js.map +1 -0
  18. package/lib/api/utils.d.ts +8 -0
  19. package/lib/api/utils.d.ts.map +1 -0
  20. package/lib/api/utils.js +132 -0
  21. package/lib/api/utils.js.map +1 -0
  22. package/lib/bridge/broadcast.d.ts.map +1 -1
  23. package/lib/bridge/broadcast.js +2 -0
  24. package/lib/bridge/broadcast.js.map +1 -1
  25. package/lib/bridge/buildOptimisticOperation.d.ts +2 -2
  26. package/lib/bridge/buildOptimisticOperation.d.ts.map +1 -1
  27. package/lib/bridge/buildOptimisticOperation.integration.test.d.ts +2 -0
  28. package/lib/bridge/buildOptimisticOperation.integration.test.d.ts.map +1 -0
  29. package/lib/bridge/buildOptimisticOperation.integration.test.js +82 -0
  30. package/lib/bridge/buildOptimisticOperation.integration.test.js.map +1 -0
  31. package/lib/bridge/buildOptimisticOperation.js +87 -5
  32. package/lib/bridge/buildOptimisticOperation.js.map +1 -1
  33. package/lib/bridge/estimateMaxSpendable.d.ts.map +1 -1
  34. package/lib/bridge/estimateMaxSpendable.js +8 -2
  35. package/lib/bridge/estimateMaxSpendable.js.map +1 -1
  36. package/lib/bridge/getTransactionStatus.d.ts +3 -3
  37. package/lib/bridge/getTransactionStatus.d.ts.map +1 -1
  38. package/lib/bridge/getTransactionStatus.js +116 -23
  39. package/lib/bridge/getTransactionStatus.js.map +1 -1
  40. package/lib/bridge/getTransactionStatus.test.d.ts +2 -0
  41. package/lib/bridge/getTransactionStatus.test.d.ts.map +1 -0
  42. package/lib/bridge/getTransactionStatus.test.js +176 -0
  43. package/lib/bridge/getTransactionStatus.test.js.map +1 -0
  44. package/lib/bridge/index.d.ts +4 -4
  45. package/lib/bridge/index.d.ts.map +1 -1
  46. package/lib/bridge/index.js +9 -6
  47. package/lib/bridge/index.js.map +1 -1
  48. package/lib/bridge/js-estimateMaxSpendable.integration.test.js +28 -44
  49. package/lib/bridge/js-estimateMaxSpendable.integration.test.js.map +1 -1
  50. package/lib/bridge/js-transaction.test.js +10 -49
  51. package/lib/bridge/js-transaction.test.js.map +1 -1
  52. package/lib/bridge/prepareTransaction.d.ts +0 -1
  53. package/lib/bridge/prepareTransaction.d.ts.map +1 -1
  54. package/lib/bridge/prepareTransaction.js +0 -1
  55. package/lib/bridge/prepareTransaction.js.map +1 -1
  56. package/lib/bridge/serialization.d.ts +7 -0
  57. package/lib/bridge/serialization.d.ts.map +1 -0
  58. package/lib/bridge/serialization.js +36 -0
  59. package/lib/bridge/serialization.js.map +1 -0
  60. package/lib/bridge/serialization.test.d.ts +2 -0
  61. package/lib/bridge/serialization.test.d.ts.map +1 -0
  62. package/lib/bridge/serialization.test.js +27 -0
  63. package/lib/bridge/serialization.test.js.map +1 -0
  64. package/lib/bridge/synchronisation.d.ts +3 -3
  65. package/lib/bridge/synchronisation.d.ts.map +1 -1
  66. package/lib/bridge/synchronisation.js +37 -15
  67. package/lib/bridge/synchronisation.js.map +1 -1
  68. package/lib/bridge/transaction.test.js +18 -59
  69. package/lib/bridge/transaction.test.js.map +1 -1
  70. package/lib/bridge/utils.d.ts +22 -8
  71. package/lib/bridge/utils.d.ts.map +1 -1
  72. package/lib/bridge/utils.integration.test.js +415 -73
  73. package/lib/bridge/utils.integration.test.js.map +1 -1
  74. package/lib/bridge/utils.js +300 -15
  75. package/lib/bridge/utils.js.map +1 -1
  76. package/lib/constants.d.ts +32 -0
  77. package/lib/constants.d.ts.map +1 -0
  78. package/lib/constants.js +37 -0
  79. package/lib/constants.js.map +1 -0
  80. package/lib/deviceTransactionConfig.d.ts.map +1 -1
  81. package/lib/deviceTransactionConfig.js +17 -15
  82. package/lib/deviceTransactionConfig.js.map +1 -1
  83. package/lib/logic.d.ts +9 -3
  84. package/lib/logic.d.ts.map +1 -1
  85. package/lib/logic.js +31 -3
  86. package/lib/logic.js.map +1 -1
  87. package/lib/logic.test.js +103 -50
  88. package/lib/logic.test.js.map +1 -1
  89. package/lib/test/fixtures/account.fixture.d.ts +19 -0
  90. package/lib/test/fixtures/account.fixture.d.ts.map +1 -0
  91. package/lib/test/fixtures/account.fixture.js +116 -0
  92. package/lib/test/fixtures/account.fixture.js.map +1 -0
  93. package/lib/test/fixtures/currency.fixture.d.ts +5 -0
  94. package/lib/test/fixtures/currency.fixture.d.ts.map +1 -0
  95. package/lib/test/fixtures/currency.fixture.js +67 -0
  96. package/lib/test/fixtures/currency.fixture.js.map +1 -0
  97. package/lib/test/fixtures/mirror.fixture.d.ts +3 -0
  98. package/lib/test/fixtures/mirror.fixture.d.ts.map +1 -0
  99. package/lib/test/fixtures/mirror.fixture.js +17 -0
  100. package/lib/test/fixtures/mirror.fixture.js.map +1 -0
  101. package/lib/test/fixtures/operation.fixture.d.ts +3 -0
  102. package/lib/test/fixtures/operation.fixture.d.ts.map +1 -0
  103. package/lib/test/fixtures/operation.fixture.js +26 -0
  104. package/lib/test/fixtures/operation.fixture.js.map +1 -0
  105. package/lib/test/fixtures/transaction.fixture.d.ts +4 -0
  106. package/lib/test/fixtures/transaction.fixture.d.ts.map +1 -0
  107. package/lib/test/fixtures/transaction.fixture.js +28 -0
  108. package/lib/test/fixtures/transaction.fixture.js.map +1 -0
  109. package/lib/types/bridge.d.ts +25 -1
  110. package/lib/types/bridge.d.ts.map +1 -1
  111. package/lib-es/api/mirror.d.ts +3 -20
  112. package/lib-es/api/mirror.d.ts.map +1 -1
  113. package/lib-es/api/mirror.js +29 -88
  114. package/lib-es/api/mirror.js.map +1 -1
  115. package/lib-es/api/mirror.test.js +60 -5
  116. package/lib-es/api/mirror.test.js.map +1 -1
  117. package/lib-es/api/network.d.ts +3 -3
  118. package/lib-es/api/network.d.ts.map +1 -1
  119. package/lib-es/api/network.js +44 -4
  120. package/lib-es/api/network.js.map +1 -1
  121. package/lib-es/api/types.d.ts +44 -0
  122. package/lib-es/api/types.d.ts.map +1 -0
  123. package/lib-es/api/types.js +2 -0
  124. package/lib-es/api/types.js.map +1 -0
  125. package/lib-es/api/utils.d.ts +8 -0
  126. package/lib-es/api/utils.d.ts.map +1 -0
  127. package/lib-es/api/utils.js +124 -0
  128. package/lib-es/api/utils.js.map +1 -0
  129. package/lib-es/bridge/broadcast.d.ts.map +1 -1
  130. package/lib-es/bridge/broadcast.js +2 -0
  131. package/lib-es/bridge/broadcast.js.map +1 -1
  132. package/lib-es/bridge/buildOptimisticOperation.d.ts +2 -2
  133. package/lib-es/bridge/buildOptimisticOperation.d.ts.map +1 -1
  134. package/lib-es/bridge/buildOptimisticOperation.integration.test.d.ts +2 -0
  135. package/lib-es/bridge/buildOptimisticOperation.integration.test.d.ts.map +1 -0
  136. package/lib-es/bridge/buildOptimisticOperation.integration.test.js +77 -0
  137. package/lib-es/bridge/buildOptimisticOperation.integration.test.js.map +1 -0
  138. package/lib-es/bridge/buildOptimisticOperation.js +84 -5
  139. package/lib-es/bridge/buildOptimisticOperation.js.map +1 -1
  140. package/lib-es/bridge/estimateMaxSpendable.d.ts.map +1 -1
  141. package/lib-es/bridge/estimateMaxSpendable.js +8 -2
  142. package/lib-es/bridge/estimateMaxSpendable.js.map +1 -1
  143. package/lib-es/bridge/getTransactionStatus.d.ts +3 -3
  144. package/lib-es/bridge/getTransactionStatus.d.ts.map +1 -1
  145. package/lib-es/bridge/getTransactionStatus.js +114 -24
  146. package/lib-es/bridge/getTransactionStatus.js.map +1 -1
  147. package/lib-es/bridge/getTransactionStatus.test.d.ts +2 -0
  148. package/lib-es/bridge/getTransactionStatus.test.d.ts.map +1 -0
  149. package/lib-es/bridge/getTransactionStatus.test.js +148 -0
  150. package/lib-es/bridge/getTransactionStatus.test.js.map +1 -0
  151. package/lib-es/bridge/index.d.ts +4 -4
  152. package/lib-es/bridge/index.d.ts.map +1 -1
  153. package/lib-es/bridge/index.js +9 -6
  154. package/lib-es/bridge/index.js.map +1 -1
  155. package/lib-es/bridge/js-estimateMaxSpendable.integration.test.js +28 -44
  156. package/lib-es/bridge/js-estimateMaxSpendable.integration.test.js.map +1 -1
  157. package/lib-es/bridge/js-transaction.test.js +10 -49
  158. package/lib-es/bridge/js-transaction.test.js.map +1 -1
  159. package/lib-es/bridge/prepareTransaction.d.ts +0 -1
  160. package/lib-es/bridge/prepareTransaction.d.ts.map +1 -1
  161. package/lib-es/bridge/prepareTransaction.js +0 -1
  162. package/lib-es/bridge/prepareTransaction.js.map +1 -1
  163. package/lib-es/bridge/serialization.d.ts +7 -0
  164. package/lib-es/bridge/serialization.d.ts.map +1 -0
  165. package/lib-es/bridge/serialization.js +29 -0
  166. package/lib-es/bridge/serialization.js.map +1 -0
  167. package/lib-es/bridge/serialization.test.d.ts +2 -0
  168. package/lib-es/bridge/serialization.test.d.ts.map +1 -0
  169. package/lib-es/bridge/serialization.test.js +25 -0
  170. package/lib-es/bridge/serialization.test.js.map +1 -0
  171. package/lib-es/bridge/synchronisation.d.ts +3 -3
  172. package/lib-es/bridge/synchronisation.d.ts.map +1 -1
  173. package/lib-es/bridge/synchronisation.js +39 -17
  174. package/lib-es/bridge/synchronisation.js.map +1 -1
  175. package/lib-es/bridge/transaction.test.js +18 -59
  176. package/lib-es/bridge/transaction.test.js.map +1 -1
  177. package/lib-es/bridge/utils.d.ts +22 -8
  178. package/lib-es/bridge/utils.d.ts.map +1 -1
  179. package/lib-es/bridge/utils.integration.test.js +416 -74
  180. package/lib-es/bridge/utils.integration.test.js.map +1 -1
  181. package/lib-es/bridge/utils.js +295 -15
  182. package/lib-es/bridge/utils.js.map +1 -1
  183. package/lib-es/constants.d.ts +32 -0
  184. package/lib-es/constants.d.ts.map +1 -0
  185. package/lib-es/constants.js +34 -0
  186. package/lib-es/constants.js.map +1 -0
  187. package/lib-es/deviceTransactionConfig.d.ts.map +1 -1
  188. package/lib-es/deviceTransactionConfig.js +17 -15
  189. package/lib-es/deviceTransactionConfig.js.map +1 -1
  190. package/lib-es/logic.d.ts +9 -3
  191. package/lib-es/logic.d.ts.map +1 -1
  192. package/lib-es/logic.js +26 -3
  193. package/lib-es/logic.js.map +1 -1
  194. package/lib-es/logic.test.js +104 -51
  195. package/lib-es/logic.test.js.map +1 -1
  196. package/lib-es/test/fixtures/account.fixture.d.ts +19 -0
  197. package/lib-es/test/fixtures/account.fixture.d.ts.map +1 -0
  198. package/lib-es/test/fixtures/account.fixture.js +107 -0
  199. package/lib-es/test/fixtures/account.fixture.js.map +1 -0
  200. package/lib-es/test/fixtures/currency.fixture.d.ts +5 -0
  201. package/lib-es/test/fixtures/currency.fixture.d.ts.map +1 -0
  202. package/lib-es/test/fixtures/currency.fixture.js +58 -0
  203. package/lib-es/test/fixtures/currency.fixture.js.map +1 -0
  204. package/lib-es/test/fixtures/mirror.fixture.d.ts +3 -0
  205. package/lib-es/test/fixtures/mirror.fixture.d.ts.map +1 -0
  206. package/lib-es/test/fixtures/mirror.fixture.js +13 -0
  207. package/lib-es/test/fixtures/mirror.fixture.js.map +1 -0
  208. package/lib-es/test/fixtures/operation.fixture.d.ts +3 -0
  209. package/lib-es/test/fixtures/operation.fixture.d.ts.map +1 -0
  210. package/lib-es/test/fixtures/operation.fixture.js +19 -0
  211. package/lib-es/test/fixtures/operation.fixture.js.map +1 -0
  212. package/lib-es/test/fixtures/transaction.fixture.d.ts +4 -0
  213. package/lib-es/test/fixtures/transaction.fixture.d.ts.map +1 -0
  214. package/lib-es/test/fixtures/transaction.fixture.js +20 -0
  215. package/lib-es/test/fixtures/transaction.fixture.js.map +1 -0
  216. package/lib-es/types/bridge.d.ts +25 -1
  217. package/lib-es/types/bridge.d.ts.map +1 -1
  218. package/package.json +12 -10
  219. package/src/api/mirror.test.ts +79 -5
  220. package/src/api/mirror.ts +30 -111
  221. package/src/api/network.ts +71 -4
  222. package/src/api/types.ts +48 -0
  223. package/src/api/utils.ts +150 -0
  224. package/src/bridge/broadcast.ts +2 -0
  225. package/src/bridge/buildOptimisticOperation.integration.test.ts +88 -0
  226. package/src/bridge/buildOptimisticOperation.ts +118 -7
  227. package/src/bridge/estimateMaxSpendable.ts +8 -2
  228. package/src/bridge/getTransactionStatus.test.ts +200 -0
  229. package/src/bridge/getTransactionStatus.ts +166 -32
  230. package/src/bridge/index.ts +13 -10
  231. package/src/bridge/js-estimateMaxSpendable.integration.test.ts +37 -46
  232. package/src/bridge/js-transaction.test.ts +13 -54
  233. package/src/bridge/prepareTransaction.ts +1 -2
  234. package/src/bridge/serialization.test.ts +39 -0
  235. package/src/bridge/serialization.ts +43 -0
  236. package/src/bridge/synchronisation.ts +65 -27
  237. package/src/bridge/transaction.test.ts +22 -64
  238. package/src/bridge/utils.integration.test.ts +525 -76
  239. package/src/bridge/utils.ts +423 -24
  240. package/src/constants.ts +35 -0
  241. package/src/deviceTransactionConfig.ts +16 -15
  242. package/src/logic.test.ts +147 -57
  243. package/src/logic.ts +58 -7
  244. package/src/test/fixtures/account.fixture.ts +123 -0
  245. package/src/test/fixtures/currency.fixture.ts +66 -0
  246. package/src/test/fixtures/mirror.fixture.ts +14 -0
  247. package/src/test/fixtures/operation.fixture.ts +20 -0
  248. package/src/test/fixtures/transaction.fixture.ts +22 -0
  249. package/src/types/bridge.ts +33 -0
@@ -1,21 +1,67 @@
1
- import { Account, Operation } from "@ledgerhq/types-live";
1
+ import invariant from "invariant";
2
+ import type { Account, Operation, OperationType, TokenAccount } from "@ledgerhq/types-live";
2
3
  import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
4
+ import { findSubAccountById, isTokenAccount } from "@ledgerhq/coin-framework/account/helpers";
5
+ import type { HederaOperationExtra, Transaction } from "../types";
3
6
  import { getEstimatedFees } from "./utils";
4
- import { Transaction } from "../types";
7
+ import { isTokenAssociateTransaction } from "../logic";
8
+ import { HEDERA_OPERATION_TYPES } from "../constants";
5
9
 
6
- export const buildOptimisticOperation = async ({
10
+ const buildOptimisticTokenAssociateOperation = async ({
11
+ account,
12
+ transaction,
13
+ }: {
14
+ account: Account;
15
+ transaction: Transaction;
16
+ }): Promise<Operation> => {
17
+ invariant(isTokenAssociateTransaction(transaction), "invalid transaction properties");
18
+
19
+ const estimatedFee = await getEstimatedFees(account, HEDERA_OPERATION_TYPES.TokenAssociate);
20
+ const value = transaction.amount;
21
+ const type: OperationType = "ASSOCIATE_TOKEN";
22
+
23
+ const operation: Operation = {
24
+ id: encodeOperationId(account.id, "", type),
25
+ hash: "",
26
+ type,
27
+ value,
28
+ fee: estimatedFee,
29
+ blockHash: null,
30
+ blockHeight: null,
31
+ senders: [account.freshAddress.toString()],
32
+ recipients: [transaction.recipient],
33
+ accountId: account.id,
34
+ date: new Date(),
35
+ extra: {
36
+ associatedTokenId: transaction.properties.token.contractAddress,
37
+ } satisfies HederaOperationExtra,
38
+ };
39
+
40
+ return operation;
41
+ };
42
+
43
+ const buildOptimisticCoinOperation = async ({
7
44
  account,
8
45
  transaction,
46
+ transactionType,
9
47
  }: {
10
48
  account: Account;
11
49
  transaction: Transaction;
50
+ transactionType?: OperationType;
12
51
  }): Promise<Operation> => {
52
+ const estimatedFee =
53
+ transactionType === "FEES"
54
+ ? transaction.amount
55
+ : await getEstimatedFees(account, HEDERA_OPERATION_TYPES.CryptoTransfer);
56
+ const value = transaction.amount;
57
+ const type: OperationType = transactionType ?? "OUT";
58
+
13
59
  const operation: Operation = {
14
- id: encodeOperationId(account.id, "", "OUT"),
60
+ id: encodeOperationId(account.id, "", type),
15
61
  hash: "",
16
- type: "OUT",
17
- value: transaction.amount,
18
- fee: await getEstimatedFees(account),
62
+ type,
63
+ value,
64
+ fee: estimatedFee,
19
65
  blockHash: null,
20
66
  blockHeight: null,
21
67
  senders: [account.freshAddress.toString()],
@@ -27,3 +73,68 @@ export const buildOptimisticOperation = async ({
27
73
 
28
74
  return operation;
29
75
  };
76
+
77
+ const buildOptimisticTokenOperation = async ({
78
+ account,
79
+ tokenAccount,
80
+ transaction,
81
+ }: {
82
+ account: Account;
83
+ tokenAccount: TokenAccount;
84
+ transaction: Transaction;
85
+ }): Promise<Operation> => {
86
+ const estimatedFee = await getEstimatedFees(account, HEDERA_OPERATION_TYPES.TokenTransfer);
87
+ const value = transaction.amount;
88
+ const type: OperationType = "OUT";
89
+
90
+ const coinOperation = await buildOptimisticCoinOperation({
91
+ account,
92
+ transaction: {
93
+ ...transaction,
94
+ recipient: tokenAccount.token.contractAddress,
95
+ amount: estimatedFee,
96
+ },
97
+ transactionType: "FEES",
98
+ });
99
+
100
+ const operation: Operation = {
101
+ ...coinOperation,
102
+ subOperations: [
103
+ {
104
+ id: encodeOperationId(tokenAccount.id, "", type),
105
+ hash: "",
106
+ type,
107
+ value,
108
+ fee: estimatedFee,
109
+ blockHash: null,
110
+ blockHeight: null,
111
+ senders: [account.freshAddress.toString()],
112
+ recipients: [transaction.recipient],
113
+ accountId: tokenAccount.id,
114
+ date: new Date(),
115
+ extra: {},
116
+ },
117
+ ],
118
+ };
119
+
120
+ return operation;
121
+ };
122
+
123
+ export const buildOptimisticOperation = async ({
124
+ account,
125
+ transaction,
126
+ }: {
127
+ account: Account;
128
+ transaction: Transaction;
129
+ }): Promise<Operation> => {
130
+ const subAccount = findSubAccountById(account, transaction.subAccountId || "");
131
+ const isTokenTransaction = isTokenAccount(subAccount);
132
+
133
+ if (isTokenAssociateTransaction(transaction)) {
134
+ return buildOptimisticTokenAssociateOperation({ account, transaction });
135
+ } else if (isTokenTransaction) {
136
+ return buildOptimisticTokenOperation({ account, tokenAccount: subAccount, transaction });
137
+ } else {
138
+ return buildOptimisticCoinOperation({ account, transaction });
139
+ }
140
+ };
@@ -1,18 +1,24 @@
1
1
  import BigNumber from "bignumber.js";
2
2
  import type { AccountBridge } from "@ledgerhq/types-live";
3
3
  import { getMainAccount } from "@ledgerhq/coin-framework/account/index";
4
+ import { isTokenAccount } from "@ledgerhq/coin-framework/account/helpers";
4
5
  import type { Transaction } from "../types";
5
6
  import { getEstimatedFees } from "./utils";
7
+ import { HEDERA_OPERATION_TYPES } from "../constants";
6
8
 
7
9
  export const estimateMaxSpendable: AccountBridge<Transaction>["estimateMaxSpendable"] = async ({
8
10
  account,
9
11
  parentAccount,
10
12
  }) => {
13
+ const mainAccount = getMainAccount(account, parentAccount);
14
+ const isTokenTransaction = isTokenAccount(account);
11
15
  const balance = account.balance;
12
16
 
13
- const mainAccount = getMainAccount(account, parentAccount);
14
- const estimatedFees = await getEstimatedFees(mainAccount);
17
+ if (isTokenTransaction) {
18
+ return Promise.resolve(balance);
19
+ }
15
20
 
21
+ const estimatedFees = await getEstimatedFees(mainAccount, HEDERA_OPERATION_TYPES.CryptoTransfer);
16
22
  let maxSpendable = balance.minus(estimatedFees);
17
23
 
18
24
  // set max spendable to 0 if negative
@@ -0,0 +1,200 @@
1
+ import BigNumber from "bignumber.js";
2
+ import {
3
+ InvalidAddress,
4
+ InvalidAddressBecauseDestinationIsAlsoSource,
5
+ AmountRequired,
6
+ NotEnoughBalance,
7
+ HederaInsufficientFundsForAssociation,
8
+ HederaRecipientTokenAssociationRequired,
9
+ HederaRecipientTokenAssociationUnverified,
10
+ } from "@ledgerhq/errors";
11
+ import { getMockedAccount, getMockedTokenAccount } from "../test/fixtures/account.fixture";
12
+ import { getMockedTokenCurrency } from "../test/fixtures/currency.fixture";
13
+ import { getMockedTransaction } from "../test/fixtures/transaction.fixture";
14
+ import { getTransactionStatus } from "./getTransactionStatus";
15
+ import * as utils from "./utils";
16
+ import { HEDERA_TRANSACTION_KINDS } from "../constants";
17
+
18
+ describe("getTransactionStatus", () => {
19
+ const mockedEstimatedFee = new BigNumber(1);
20
+ const mockedUsdRate = new BigNumber(1);
21
+ const validRecipientAddress = "0.0.1234567";
22
+ const invalidRecipientAddress = "invalid_address";
23
+
24
+ beforeEach(() => {
25
+ jest.clearAllMocks();
26
+
27
+ jest.spyOn(utils, "getCurrencyToUSDRate").mockResolvedValueOnce(mockedUsdRate);
28
+ jest.spyOn(utils, "getEstimatedFees").mockResolvedValueOnce(mockedEstimatedFee);
29
+ });
30
+
31
+ test("coin transfer with valid recipient and sufficient balance completes successfully", async () => {
32
+ const mockedAccount = getMockedAccount({ balance: new BigNumber(1000) });
33
+ const mockedTransaction = getMockedTransaction({
34
+ recipient: validRecipientAddress,
35
+ amount: new BigNumber(100),
36
+ });
37
+
38
+ const result = await getTransactionStatus(mockedAccount, mockedTransaction);
39
+
40
+ expect(result.errors).toEqual({});
41
+ expect(result.warnings).toEqual({});
42
+ expect(result.amount).toEqual(new BigNumber(100));
43
+ expect(result.totalSpent.isGreaterThan(100)).toBe(true);
44
+ });
45
+
46
+ test("token transfer with valid recipient and sufficient balance completes successfully", async () => {
47
+ jest.spyOn(utils, "checkAccountTokenAssociationStatus").mockResolvedValueOnce(true);
48
+
49
+ const tokenCurrency = getMockedTokenCurrency();
50
+ const tokenAccount = getMockedTokenAccount(tokenCurrency, { balance: new BigNumber(500) });
51
+ const account = getMockedAccount({ balance: new BigNumber(1000), subAccounts: [tokenAccount] });
52
+ const transaction = getMockedTransaction({
53
+ subAccountId: tokenAccount.id,
54
+ recipient: validRecipientAddress,
55
+ amount: new BigNumber(200),
56
+ });
57
+
58
+ const result = await getTransactionStatus(account, transaction);
59
+
60
+ expect(result.errors).toEqual({});
61
+ expect(result.warnings).toEqual({});
62
+ expect(result.amount).toEqual(new BigNumber(200));
63
+ });
64
+
65
+ test("token associate transaction with sufficient USD worth completes successfully", async () => {
66
+ const mockedTokenCurrency = getMockedTokenCurrency();
67
+ const mockedAccount = getMockedAccount();
68
+ const mockedTransaction = getMockedTransaction({
69
+ properties: {
70
+ name: HEDERA_TRANSACTION_KINDS.TokenAssociate.name,
71
+ token: mockedTokenCurrency,
72
+ },
73
+ });
74
+
75
+ const result = await getTransactionStatus(mockedAccount, mockedTransaction);
76
+
77
+ expect(result.amount).toEqual(new BigNumber(0));
78
+ expect(result.errors).toEqual({});
79
+ expect(result.warnings).toEqual({});
80
+ expect(result.totalSpent).toEqual(mockedEstimatedFee);
81
+ expect(result.estimatedFees).toEqual(mockedEstimatedFee);
82
+ });
83
+
84
+ test("adds error for invalid recipient address", async () => {
85
+ const mockedAccount = getMockedAccount();
86
+ const mockedTransaction = getMockedTransaction({ recipient: invalidRecipientAddress });
87
+
88
+ const result = await getTransactionStatus(mockedAccount, mockedTransaction);
89
+
90
+ expect(result.errors.recipient).toBeInstanceOf(InvalidAddress);
91
+ });
92
+
93
+ test("adds error for self transfers", async () => {
94
+ const mockedAccount = getMockedAccount();
95
+ const mockedTransaction = getMockedTransaction({
96
+ recipient: mockedAccount.freshAddress,
97
+ });
98
+
99
+ const result = await getTransactionStatus(mockedAccount, mockedTransaction);
100
+
101
+ expect(result.errors.recipient).toBeInstanceOf(InvalidAddressBecauseDestinationIsAlsoSource);
102
+ });
103
+
104
+ test("adds error during coin transfer with insufficient balance", async () => {
105
+ const mockedAccount = getMockedAccount({ balance: new BigNumber(0) });
106
+ const mockedTransaction = getMockedTransaction({
107
+ amount: new BigNumber(100),
108
+ recipient: validRecipientAddress,
109
+ });
110
+
111
+ const result = await getTransactionStatus(mockedAccount, mockedTransaction);
112
+
113
+ expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance);
114
+ });
115
+
116
+ test("adds error if USD balance is too low for token association", async () => {
117
+ const mockedTokenCurrency = getMockedTokenCurrency();
118
+ const mockedAccount = getMockedAccount({ balance: new BigNumber(0) });
119
+ const mockedTransaction = getMockedTransaction({
120
+ properties: {
121
+ name: HEDERA_TRANSACTION_KINDS.TokenAssociate.name,
122
+ token: mockedTokenCurrency,
123
+ },
124
+ });
125
+
126
+ const result = await getTransactionStatus(mockedAccount, mockedTransaction);
127
+
128
+ expect(result.errors.insufficientAssociateBalance).toBeInstanceOf(
129
+ HederaInsufficientFundsForAssociation,
130
+ );
131
+ });
132
+
133
+ test("adds warning during token transfer if recipient has no token associated", async () => {
134
+ jest.spyOn(utils, "checkAccountTokenAssociationStatus").mockResolvedValueOnce(false);
135
+
136
+ const mockedTokenCurrency = getMockedTokenCurrency();
137
+ const mockedTokenAccount = getMockedTokenAccount(mockedTokenCurrency);
138
+ const mockedAccount = getMockedAccount({ subAccounts: [mockedTokenAccount] });
139
+ const mockedTransaction = getMockedTransaction({
140
+ subAccountId: mockedTokenAccount.id,
141
+ recipient: validRecipientAddress,
142
+ });
143
+
144
+ const result = await getTransactionStatus(mockedAccount, mockedTransaction);
145
+
146
+ expect(result.warnings.missingAssociation).toBeInstanceOf(
147
+ HederaRecipientTokenAssociationRequired,
148
+ );
149
+ });
150
+
151
+ test("adds warning if token association status can't be verified", async () => {
152
+ jest
153
+ .spyOn(utils, "checkAccountTokenAssociationStatus")
154
+ .mockRejectedValueOnce(new HederaRecipientTokenAssociationUnverified());
155
+
156
+ const mockedTokenCurrency = getMockedTokenCurrency();
157
+ const mockedTokenAccount = getMockedTokenAccount(mockedTokenCurrency);
158
+ const mockedAccount = getMockedAccount({ subAccounts: [mockedTokenAccount] });
159
+ const mockedTransaction = getMockedTransaction({
160
+ subAccountId: mockedTokenAccount.id,
161
+ recipient: validRecipientAddress,
162
+ });
163
+
164
+ const result = await getTransactionStatus(mockedAccount, mockedTransaction);
165
+
166
+ expect(result.warnings.unverifiedAssociation).toBeInstanceOf(
167
+ HederaRecipientTokenAssociationUnverified,
168
+ );
169
+ });
170
+
171
+ test("adds error during token transfer with insufficient balance", async () => {
172
+ const mockedTokenCurrency = getMockedTokenCurrency();
173
+ const mockedTokenAccount = getMockedTokenAccount(mockedTokenCurrency, {
174
+ balance: new BigNumber(0),
175
+ });
176
+ const mockedAccount = getMockedAccount({ subAccounts: [mockedTokenAccount] });
177
+ const mockedTransaction = getMockedTransaction({
178
+ subAccountId: mockedTokenAccount.id,
179
+ recipient: validRecipientAddress,
180
+ amount: new BigNumber(100),
181
+ });
182
+
183
+ const result = await getTransactionStatus(mockedAccount, mockedTransaction);
184
+
185
+ expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance);
186
+ });
187
+
188
+ test("adds error if amount is zero and useAllAmount is false", async () => {
189
+ const mockedAccount = getMockedAccount();
190
+ const mockedTransaction = getMockedTransaction({
191
+ recipient: validRecipientAddress,
192
+ amount: new BigNumber(0),
193
+ useAllAmount: false,
194
+ });
195
+
196
+ const result = await getTransactionStatus(mockedAccount, mockedTransaction);
197
+
198
+ expect(result.errors.amount).toBeInstanceOf(AmountRequired);
199
+ });
200
+ });
@@ -1,55 +1,189 @@
1
+ import BigNumber from "bignumber.js";
2
+ import { AccountId } from "@hashgraph/sdk";
1
3
  import {
2
4
  AmountRequired,
3
5
  NotEnoughBalance,
4
6
  InvalidAddress,
5
7
  InvalidAddressBecauseDestinationIsAlsoSource,
6
8
  RecipientRequired,
9
+ HederaInsufficientFundsForAssociation,
10
+ HederaRecipientTokenAssociationRequired,
11
+ HederaRecipientTokenAssociationUnverified,
7
12
  } from "@ledgerhq/errors";
8
- import { AccountId } from "@hashgraph/sdk";
9
- import type { AccountBridge } from "@ledgerhq/types-live";
10
- import { calculateAmount, getEstimatedFees } from "./utils";
11
- import type { Transaction } from "../types";
12
-
13
- export const getTransactionStatus: AccountBridge<Transaction>["getTransactionStatus"] = async (
14
- account,
15
- transaction,
16
- ) => {
17
- const errors: Record<string, Error> = {};
18
-
19
- if (!transaction.recipient || transaction.recipient.length === 0) {
20
- errors.recipient = new RecipientRequired("");
21
- } else {
22
- if (account.freshAddress === transaction.recipient) {
23
- errors.recipient = new InvalidAddressBecauseDestinationIsAlsoSource("");
13
+ import type { Account, AccountBridge, TokenAccount } from "@ledgerhq/types-live";
14
+ import { findSubAccountById, isTokenAccount } from "@ledgerhq/coin-framework/account";
15
+ import { getEnv } from "@ledgerhq/live-env";
16
+ import { isTokenAssociateTransaction, isTokenAssociationRequired } from "../logic";
17
+ import type { TokenAssociateProperties, Transaction, TransactionStatus } from "../types";
18
+ import {
19
+ calculateAmount,
20
+ checkAccountTokenAssociationStatus,
21
+ getCurrencyToUSDRate,
22
+ getEstimatedFees,
23
+ } from "./utils";
24
+ import { HEDERA_OPERATION_TYPES } from "../constants";
25
+
26
+ type Errors = Record<string, Error>;
27
+ type Warnings = Record<string, Error>;
28
+
29
+ function validateRecipient(account: Account, recipient: string): Error | null {
30
+ if (!recipient || recipient.length === 0) {
31
+ return new RecipientRequired();
32
+ }
33
+
34
+ if (account.freshAddress === recipient) {
35
+ return new InvalidAddressBecauseDestinationIsAlsoSource();
36
+ }
37
+
38
+ try {
39
+ AccountId.fromString(recipient);
40
+ } catch (err) {
41
+ return new InvalidAddress("", {
42
+ currencyName: account.currency.name,
43
+ });
44
+ }
45
+
46
+ return null;
47
+ }
48
+
49
+ async function handleTokenAssociateTransaction(
50
+ account: Account,
51
+ transaction: Extract<Required<Transaction>, { properties: TokenAssociateProperties }>,
52
+ ): Promise<TransactionStatus> {
53
+ const errors: Errors = {};
54
+ const warnings: Warnings = {};
55
+
56
+ const [usdRate, estimatedFees] = await Promise.all([
57
+ getCurrencyToUSDRate(account.currency),
58
+ getEstimatedFees(account, HEDERA_OPERATION_TYPES.TokenAssociate),
59
+ ]);
60
+
61
+ const amount = BigNumber(0);
62
+ const totalSpent = amount.plus(estimatedFees);
63
+ const isAssociationFlow = isTokenAssociationRequired(account, transaction.properties.token);
64
+
65
+ if (isAssociationFlow) {
66
+ const hbarBalance = account.balance.dividedBy(10 ** account.currency.units[0].magnitude);
67
+ const currentWorthInUSD = usdRate ? hbarBalance.multipliedBy(usdRate) : new BigNumber(0);
68
+ const requiredWorthInUSD = getEnv("HEDERA_TOKEN_ASSOCIATION_MIN_USD");
69
+
70
+ if (currentWorthInUSD.isLessThan(requiredWorthInUSD)) {
71
+ errors.insufficientAssociateBalance = new HederaInsufficientFundsForAssociation("", {
72
+ requiredWorthInUSD,
73
+ });
24
74
  }
75
+ }
76
+
77
+ return {
78
+ amount,
79
+ totalSpent,
80
+ estimatedFees,
81
+ errors,
82
+ warnings,
83
+ };
84
+ }
85
+
86
+ async function handleTokenTransaction(
87
+ account: Account,
88
+ subAccount: TokenAccount,
89
+ transaction: Transaction,
90
+ ): Promise<TransactionStatus> {
91
+ const errors: Errors = {};
92
+ const warnings: Warnings = {};
93
+ const [calculatedAmount, estimatedFees] = await Promise.all([
94
+ calculateAmount({ transaction, account }),
95
+ getEstimatedFees(account, HEDERA_OPERATION_TYPES.TokenTransfer),
96
+ ]);
97
+
98
+ const recipientError = validateRecipient(account, transaction.recipient);
99
+
100
+ if (recipientError) {
101
+ errors.recipient = recipientError;
102
+ }
25
103
 
104
+ if (!errors.recipient) {
26
105
  try {
27
- AccountId.fromString(transaction.recipient);
28
- } catch (err) {
29
- errors.recipient = new InvalidAddress("", {
30
- currencyName: account.currency.name,
31
- });
106
+ const hasRecipientTokenAssociated = await checkAccountTokenAssociationStatus(
107
+ transaction.recipient,
108
+ subAccount.token.contractAddress,
109
+ );
110
+
111
+ if (!hasRecipientTokenAssociated) {
112
+ warnings.missingAssociation = new HederaRecipientTokenAssociationRequired();
113
+ }
114
+ } catch {
115
+ warnings.unverifiedAssociation = new HederaRecipientTokenAssociationUnverified();
32
116
  }
33
117
  }
34
118
 
35
- const { amount, totalSpent } = await calculateAmount({
36
- transaction,
37
- account,
38
- });
119
+ if (transaction.amount.eq(0)) {
120
+ errors.amount = new AmountRequired();
121
+ }
122
+
123
+ if (subAccount.balance.isLessThan(calculatedAmount.totalSpent)) {
124
+ errors.amount = new NotEnoughBalance();
125
+ }
126
+
127
+ if (account.balance.isLessThan(estimatedFees)) {
128
+ errors.amount = new NotEnoughBalance();
129
+ }
130
+
131
+ return {
132
+ amount: calculatedAmount.amount,
133
+ totalSpent: calculatedAmount.totalSpent,
134
+ estimatedFees,
135
+ errors,
136
+ warnings,
137
+ };
138
+ }
139
+
140
+ async function handleCoinTransaction(
141
+ account: Account,
142
+ transaction: Transaction,
143
+ ): Promise<TransactionStatus> {
144
+ const errors: Errors = {};
145
+ const warnings: Warnings = {};
146
+ const [calculatedAmount, estimatedFees] = await Promise.all([
147
+ calculateAmount({ transaction, account }),
148
+ getEstimatedFees(account, HEDERA_OPERATION_TYPES.CryptoTransfer),
149
+ ]);
150
+
151
+ const recipientError = validateRecipient(account, transaction.recipient);
152
+
153
+ if (recipientError) {
154
+ errors.recipient = recipientError;
155
+ }
39
156
 
40
157
  if (transaction.amount.eq(0) && !transaction.useAllAmount) {
41
158
  errors.amount = new AmountRequired();
42
- } else if (account.balance.isLessThan(totalSpent)) {
43
- errors.amount = new NotEnoughBalance("");
44
159
  }
45
160
 
46
- const estimatedFees = await getEstimatedFees(account);
161
+ if (account.balance.isLessThan(calculatedAmount.totalSpent)) {
162
+ errors.amount = new NotEnoughBalance("");
163
+ }
47
164
 
48
165
  return {
49
- amount,
50
- errors,
166
+ amount: calculatedAmount.amount,
167
+ totalSpent: calculatedAmount.totalSpent,
51
168
  estimatedFees,
52
- totalSpent,
53
- warnings: {},
169
+ errors,
170
+ warnings,
54
171
  };
172
+ }
173
+
174
+ export const getTransactionStatus: AccountBridge<
175
+ Transaction,
176
+ Account,
177
+ TransactionStatus
178
+ >["getTransactionStatus"] = async (account, transaction) => {
179
+ const subAccount = findSubAccountById(account, transaction?.subAccountId || "");
180
+ const isTokenTransaction = isTokenAccount(subAccount);
181
+
182
+ if (isTokenAssociateTransaction(transaction)) {
183
+ return handleTokenAssociateTransaction(account, transaction);
184
+ } else if (isTokenTransaction) {
185
+ return handleTokenTransaction(account, subAccount, transaction);
186
+ } else {
187
+ return handleCoinTransaction(account, transaction);
188
+ }
55
189
  };
@@ -4,19 +4,20 @@ import {
4
4
  makeSync,
5
5
  updateTransaction,
6
6
  } from "@ledgerhq/coin-framework/bridge/jsHelpers";
7
- import resolver from "../signer/index";
8
7
  import getAddressWrapper from "@ledgerhq/coin-framework/bridge/getAddressWrapper";
9
- import { SignerContext } from "@ledgerhq/coin-framework/signer";
10
- import type { Account, AccountBridge, CurrencyBridge } from "@ledgerhq/types-live";
11
- import type { Transaction, TransactionStatus, HederaSigner } from "../types";
12
- import { getTransactionStatus } from "./getTransactionStatus";
8
+ import type { SignerContext } from "@ledgerhq/coin-framework/signer";
9
+ import type { AccountBridge, CurrencyBridge } from "@ledgerhq/types-live";
10
+ import { broadcast } from "./broadcast";
11
+ import { createTransaction } from "./createTransaction";
13
12
  import { estimateMaxSpendable } from "./estimateMaxSpendable";
13
+ import { getTransactionStatus } from "./getTransactionStatus";
14
14
  import { prepareTransaction } from "./prepareTransaction";
15
- import { createTransaction } from "./createTransaction";
16
- import { getAccountShape, buildIterateResult } from "./synchronisation";
17
- import { buildSignOperation } from "./signOperation";
18
- import { broadcast } from "./broadcast";
19
15
  import { receive } from "./receive";
16
+ import { buildSignOperation } from "./signOperation";
17
+ import { getAccountShape, buildIterateResult } from "./synchronisation";
18
+ import { assignFromAccountRaw, assignToAccountRaw } from "./serialization";
19
+ import resolver from "../signer/index";
20
+ import type { Transaction, TransactionStatus, HederaSigner, HederaAccount } from "../types";
20
21
 
21
22
  function buildCurrencyBridge(signerContext: SignerContext<HederaSigner>): CurrencyBridge {
22
23
  const getAddress = resolver(signerContext);
@@ -38,7 +39,7 @@ const sync = makeSync({ getAccountShape });
38
39
 
39
40
  function buildAccountBridge(
40
41
  signerContext: SignerContext<HederaSigner>,
41
- ): AccountBridge<Transaction, Account, TransactionStatus> {
42
+ ): AccountBridge<Transaction, HederaAccount, TransactionStatus> {
42
43
  const getAddress = resolver(signerContext);
43
44
 
44
45
  const signOperation = buildSignOperation(signerContext);
@@ -49,6 +50,8 @@ function buildAccountBridge(
49
50
  updateTransaction,
50
51
  getTransactionStatus,
51
52
  prepareTransaction,
53
+ assignToAccountRaw,
54
+ assignFromAccountRaw,
52
55
  sync,
53
56
  receive: receive(getAddressWrapper(getAddress)),
54
57
  signOperation,