@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.
- package/.eslintrc.js +1 -0
- package/CHANGELOG.md +10 -0
- package/lib/api/mirror.d.ts +3 -20
- package/lib/api/mirror.d.ts.map +1 -1
- package/lib/api/mirror.js +32 -90
- package/lib/api/mirror.js.map +1 -1
- package/lib/api/mirror.test.js +59 -4
- package/lib/api/mirror.test.js.map +1 -1
- package/lib/api/network.d.ts +3 -3
- package/lib/api/network.d.ts.map +1 -1
- package/lib/api/network.js +46 -3
- package/lib/api/network.js.map +1 -1
- package/lib/api/types.d.ts +44 -0
- package/lib/api/types.d.ts.map +1 -0
- package/lib/api/types.js +3 -0
- package/lib/api/types.js.map +1 -0
- package/lib/api/utils.d.ts +8 -0
- package/lib/api/utils.d.ts.map +1 -0
- package/lib/api/utils.js +132 -0
- package/lib/api/utils.js.map +1 -0
- package/lib/bridge/broadcast.d.ts.map +1 -1
- package/lib/bridge/broadcast.js +2 -0
- package/lib/bridge/broadcast.js.map +1 -1
- package/lib/bridge/buildOptimisticOperation.d.ts +2 -2
- package/lib/bridge/buildOptimisticOperation.d.ts.map +1 -1
- package/lib/bridge/buildOptimisticOperation.integration.test.d.ts +2 -0
- package/lib/bridge/buildOptimisticOperation.integration.test.d.ts.map +1 -0
- package/lib/bridge/buildOptimisticOperation.integration.test.js +82 -0
- package/lib/bridge/buildOptimisticOperation.integration.test.js.map +1 -0
- package/lib/bridge/buildOptimisticOperation.js +87 -5
- package/lib/bridge/buildOptimisticOperation.js.map +1 -1
- package/lib/bridge/estimateMaxSpendable.d.ts.map +1 -1
- package/lib/bridge/estimateMaxSpendable.js +8 -2
- package/lib/bridge/estimateMaxSpendable.js.map +1 -1
- package/lib/bridge/getTransactionStatus.d.ts +3 -3
- package/lib/bridge/getTransactionStatus.d.ts.map +1 -1
- package/lib/bridge/getTransactionStatus.js +116 -23
- package/lib/bridge/getTransactionStatus.js.map +1 -1
- package/lib/bridge/getTransactionStatus.test.d.ts +2 -0
- package/lib/bridge/getTransactionStatus.test.d.ts.map +1 -0
- package/lib/bridge/getTransactionStatus.test.js +176 -0
- package/lib/bridge/getTransactionStatus.test.js.map +1 -0
- package/lib/bridge/index.d.ts +4 -4
- package/lib/bridge/index.d.ts.map +1 -1
- package/lib/bridge/index.js +9 -6
- package/lib/bridge/index.js.map +1 -1
- package/lib/bridge/js-estimateMaxSpendable.integration.test.js +28 -44
- package/lib/bridge/js-estimateMaxSpendable.integration.test.js.map +1 -1
- package/lib/bridge/js-transaction.test.js +10 -49
- package/lib/bridge/js-transaction.test.js.map +1 -1
- package/lib/bridge/prepareTransaction.d.ts +0 -1
- package/lib/bridge/prepareTransaction.d.ts.map +1 -1
- package/lib/bridge/prepareTransaction.js +0 -1
- package/lib/bridge/prepareTransaction.js.map +1 -1
- package/lib/bridge/serialization.d.ts +7 -0
- package/lib/bridge/serialization.d.ts.map +1 -0
- package/lib/bridge/serialization.js +36 -0
- package/lib/bridge/serialization.js.map +1 -0
- package/lib/bridge/serialization.test.d.ts +2 -0
- package/lib/bridge/serialization.test.d.ts.map +1 -0
- package/lib/bridge/serialization.test.js +27 -0
- package/lib/bridge/serialization.test.js.map +1 -0
- package/lib/bridge/synchronisation.d.ts +3 -3
- package/lib/bridge/synchronisation.d.ts.map +1 -1
- package/lib/bridge/synchronisation.js +37 -15
- package/lib/bridge/synchronisation.js.map +1 -1
- package/lib/bridge/transaction.test.js +18 -59
- package/lib/bridge/transaction.test.js.map +1 -1
- package/lib/bridge/utils.d.ts +22 -8
- package/lib/bridge/utils.d.ts.map +1 -1
- package/lib/bridge/utils.integration.test.js +415 -73
- package/lib/bridge/utils.integration.test.js.map +1 -1
- package/lib/bridge/utils.js +300 -15
- package/lib/bridge/utils.js.map +1 -1
- package/lib/constants.d.ts +32 -0
- package/lib/constants.d.ts.map +1 -0
- package/lib/constants.js +37 -0
- package/lib/constants.js.map +1 -0
- package/lib/deviceTransactionConfig.d.ts.map +1 -1
- package/lib/deviceTransactionConfig.js +17 -15
- package/lib/deviceTransactionConfig.js.map +1 -1
- package/lib/logic.d.ts +9 -3
- package/lib/logic.d.ts.map +1 -1
- package/lib/logic.js +31 -3
- package/lib/logic.js.map +1 -1
- package/lib/logic.test.js +103 -50
- package/lib/logic.test.js.map +1 -1
- package/lib/test/fixtures/account.fixture.d.ts +19 -0
- package/lib/test/fixtures/account.fixture.d.ts.map +1 -0
- package/lib/test/fixtures/account.fixture.js +116 -0
- package/lib/test/fixtures/account.fixture.js.map +1 -0
- package/lib/test/fixtures/currency.fixture.d.ts +5 -0
- package/lib/test/fixtures/currency.fixture.d.ts.map +1 -0
- package/lib/test/fixtures/currency.fixture.js +67 -0
- package/lib/test/fixtures/currency.fixture.js.map +1 -0
- package/lib/test/fixtures/mirror.fixture.d.ts +3 -0
- package/lib/test/fixtures/mirror.fixture.d.ts.map +1 -0
- package/lib/test/fixtures/mirror.fixture.js +17 -0
- package/lib/test/fixtures/mirror.fixture.js.map +1 -0
- package/lib/test/fixtures/operation.fixture.d.ts +3 -0
- package/lib/test/fixtures/operation.fixture.d.ts.map +1 -0
- package/lib/test/fixtures/operation.fixture.js +26 -0
- package/lib/test/fixtures/operation.fixture.js.map +1 -0
- package/lib/test/fixtures/transaction.fixture.d.ts +4 -0
- package/lib/test/fixtures/transaction.fixture.d.ts.map +1 -0
- package/lib/test/fixtures/transaction.fixture.js +28 -0
- package/lib/test/fixtures/transaction.fixture.js.map +1 -0
- package/lib/types/bridge.d.ts +25 -1
- package/lib/types/bridge.d.ts.map +1 -1
- package/lib-es/api/mirror.d.ts +3 -20
- package/lib-es/api/mirror.d.ts.map +1 -1
- package/lib-es/api/mirror.js +29 -88
- package/lib-es/api/mirror.js.map +1 -1
- package/lib-es/api/mirror.test.js +60 -5
- package/lib-es/api/mirror.test.js.map +1 -1
- package/lib-es/api/network.d.ts +3 -3
- package/lib-es/api/network.d.ts.map +1 -1
- package/lib-es/api/network.js +44 -4
- package/lib-es/api/network.js.map +1 -1
- package/lib-es/api/types.d.ts +44 -0
- package/lib-es/api/types.d.ts.map +1 -0
- package/lib-es/api/types.js +2 -0
- package/lib-es/api/types.js.map +1 -0
- package/lib-es/api/utils.d.ts +8 -0
- package/lib-es/api/utils.d.ts.map +1 -0
- package/lib-es/api/utils.js +124 -0
- package/lib-es/api/utils.js.map +1 -0
- package/lib-es/bridge/broadcast.d.ts.map +1 -1
- package/lib-es/bridge/broadcast.js +2 -0
- package/lib-es/bridge/broadcast.js.map +1 -1
- package/lib-es/bridge/buildOptimisticOperation.d.ts +2 -2
- package/lib-es/bridge/buildOptimisticOperation.d.ts.map +1 -1
- package/lib-es/bridge/buildOptimisticOperation.integration.test.d.ts +2 -0
- package/lib-es/bridge/buildOptimisticOperation.integration.test.d.ts.map +1 -0
- package/lib-es/bridge/buildOptimisticOperation.integration.test.js +77 -0
- package/lib-es/bridge/buildOptimisticOperation.integration.test.js.map +1 -0
- package/lib-es/bridge/buildOptimisticOperation.js +84 -5
- package/lib-es/bridge/buildOptimisticOperation.js.map +1 -1
- package/lib-es/bridge/estimateMaxSpendable.d.ts.map +1 -1
- package/lib-es/bridge/estimateMaxSpendable.js +8 -2
- package/lib-es/bridge/estimateMaxSpendable.js.map +1 -1
- package/lib-es/bridge/getTransactionStatus.d.ts +3 -3
- package/lib-es/bridge/getTransactionStatus.d.ts.map +1 -1
- package/lib-es/bridge/getTransactionStatus.js +114 -24
- package/lib-es/bridge/getTransactionStatus.js.map +1 -1
- package/lib-es/bridge/getTransactionStatus.test.d.ts +2 -0
- package/lib-es/bridge/getTransactionStatus.test.d.ts.map +1 -0
- package/lib-es/bridge/getTransactionStatus.test.js +148 -0
- package/lib-es/bridge/getTransactionStatus.test.js.map +1 -0
- package/lib-es/bridge/index.d.ts +4 -4
- package/lib-es/bridge/index.d.ts.map +1 -1
- package/lib-es/bridge/index.js +9 -6
- package/lib-es/bridge/index.js.map +1 -1
- package/lib-es/bridge/js-estimateMaxSpendable.integration.test.js +28 -44
- package/lib-es/bridge/js-estimateMaxSpendable.integration.test.js.map +1 -1
- package/lib-es/bridge/js-transaction.test.js +10 -49
- package/lib-es/bridge/js-transaction.test.js.map +1 -1
- package/lib-es/bridge/prepareTransaction.d.ts +0 -1
- package/lib-es/bridge/prepareTransaction.d.ts.map +1 -1
- package/lib-es/bridge/prepareTransaction.js +0 -1
- package/lib-es/bridge/prepareTransaction.js.map +1 -1
- package/lib-es/bridge/serialization.d.ts +7 -0
- package/lib-es/bridge/serialization.d.ts.map +1 -0
- package/lib-es/bridge/serialization.js +29 -0
- package/lib-es/bridge/serialization.js.map +1 -0
- package/lib-es/bridge/serialization.test.d.ts +2 -0
- package/lib-es/bridge/serialization.test.d.ts.map +1 -0
- package/lib-es/bridge/serialization.test.js +25 -0
- package/lib-es/bridge/serialization.test.js.map +1 -0
- package/lib-es/bridge/synchronisation.d.ts +3 -3
- package/lib-es/bridge/synchronisation.d.ts.map +1 -1
- package/lib-es/bridge/synchronisation.js +39 -17
- package/lib-es/bridge/synchronisation.js.map +1 -1
- package/lib-es/bridge/transaction.test.js +18 -59
- package/lib-es/bridge/transaction.test.js.map +1 -1
- package/lib-es/bridge/utils.d.ts +22 -8
- package/lib-es/bridge/utils.d.ts.map +1 -1
- package/lib-es/bridge/utils.integration.test.js +416 -74
- package/lib-es/bridge/utils.integration.test.js.map +1 -1
- package/lib-es/bridge/utils.js +295 -15
- package/lib-es/bridge/utils.js.map +1 -1
- package/lib-es/constants.d.ts +32 -0
- package/lib-es/constants.d.ts.map +1 -0
- package/lib-es/constants.js +34 -0
- package/lib-es/constants.js.map +1 -0
- package/lib-es/deviceTransactionConfig.d.ts.map +1 -1
- package/lib-es/deviceTransactionConfig.js +17 -15
- package/lib-es/deviceTransactionConfig.js.map +1 -1
- package/lib-es/logic.d.ts +9 -3
- package/lib-es/logic.d.ts.map +1 -1
- package/lib-es/logic.js +26 -3
- package/lib-es/logic.js.map +1 -1
- package/lib-es/logic.test.js +104 -51
- package/lib-es/logic.test.js.map +1 -1
- package/lib-es/test/fixtures/account.fixture.d.ts +19 -0
- package/lib-es/test/fixtures/account.fixture.d.ts.map +1 -0
- package/lib-es/test/fixtures/account.fixture.js +107 -0
- package/lib-es/test/fixtures/account.fixture.js.map +1 -0
- package/lib-es/test/fixtures/currency.fixture.d.ts +5 -0
- package/lib-es/test/fixtures/currency.fixture.d.ts.map +1 -0
- package/lib-es/test/fixtures/currency.fixture.js +58 -0
- package/lib-es/test/fixtures/currency.fixture.js.map +1 -0
- package/lib-es/test/fixtures/mirror.fixture.d.ts +3 -0
- package/lib-es/test/fixtures/mirror.fixture.d.ts.map +1 -0
- package/lib-es/test/fixtures/mirror.fixture.js +13 -0
- package/lib-es/test/fixtures/mirror.fixture.js.map +1 -0
- package/lib-es/test/fixtures/operation.fixture.d.ts +3 -0
- package/lib-es/test/fixtures/operation.fixture.d.ts.map +1 -0
- package/lib-es/test/fixtures/operation.fixture.js +19 -0
- package/lib-es/test/fixtures/operation.fixture.js.map +1 -0
- package/lib-es/test/fixtures/transaction.fixture.d.ts +4 -0
- package/lib-es/test/fixtures/transaction.fixture.d.ts.map +1 -0
- package/lib-es/test/fixtures/transaction.fixture.js +20 -0
- package/lib-es/test/fixtures/transaction.fixture.js.map +1 -0
- package/lib-es/types/bridge.d.ts +25 -1
- package/lib-es/types/bridge.d.ts.map +1 -1
- package/package.json +11 -9
- package/src/api/mirror.test.ts +79 -5
- package/src/api/mirror.ts +30 -111
- package/src/api/network.ts +71 -4
- package/src/api/types.ts +48 -0
- package/src/api/utils.ts +150 -0
- package/src/bridge/broadcast.ts +2 -0
- package/src/bridge/buildOptimisticOperation.integration.test.ts +88 -0
- package/src/bridge/buildOptimisticOperation.ts +118 -7
- package/src/bridge/estimateMaxSpendable.ts +8 -2
- package/src/bridge/getTransactionStatus.test.ts +200 -0
- package/src/bridge/getTransactionStatus.ts +166 -32
- package/src/bridge/index.ts +13 -10
- package/src/bridge/js-estimateMaxSpendable.integration.test.ts +37 -46
- package/src/bridge/js-transaction.test.ts +13 -54
- package/src/bridge/prepareTransaction.ts +1 -2
- package/src/bridge/serialization.test.ts +39 -0
- package/src/bridge/serialization.ts +43 -0
- package/src/bridge/synchronisation.ts +65 -27
- package/src/bridge/transaction.test.ts +22 -64
- package/src/bridge/utils.integration.test.ts +525 -76
- package/src/bridge/utils.ts +423 -24
- package/src/constants.ts +35 -0
- package/src/deviceTransactionConfig.ts +16 -15
- package/src/logic.test.ts +147 -57
- package/src/logic.ts +58 -7
- package/src/test/fixtures/account.fixture.ts +123 -0
- package/src/test/fixtures/currency.fixture.ts +66 -0
- package/src/test/fixtures/mirror.fixture.ts +14 -0
- package/src/test/fixtures/operation.fixture.ts +20 -0
- package/src/test/fixtures/transaction.fixture.ts +22 -0
- package/src/types/bridge.ts +33 -0
package/src/bridge/utils.ts
CHANGED
|
@@ -1,55 +1,141 @@
|
|
|
1
1
|
import BigNumber from "bignumber.js";
|
|
2
|
-
import
|
|
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 {
|
|
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
|
-
|
|
29
|
+
const ESTIMATED_FEE_SAFETY_RATE = 2;
|
|
9
30
|
|
|
10
|
-
|
|
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
|
|
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 (
|
|
21
|
-
return new BigNumber(
|
|
22
|
-
.dividedBy(new BigNumber(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
80
|
+
const calculateCoinAmount = async ({
|
|
35
81
|
account,
|
|
36
82
|
transaction,
|
|
83
|
+
operationType,
|
|
37
84
|
}: {
|
|
38
85
|
account: Account;
|
|
39
86
|
transaction: Transaction;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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(
|
|
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
|
+
);
|
package/src/constants.ts
ADDED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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: "
|
|
32
|
-
label: "
|
|
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",
|