@ledgerhq/coin-hedera
Version:
Ledger Hedera Coin integration
272 lines • 13.8 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.postSync = exports.buildIterateResult = exports.getAccountShape = void 0;
const account_1 = require("@ledgerhq/ledger-wallet-framework/account");
const jsHelpers_1 = require("@ledgerhq/ledger-wallet-framework/bridge/jsHelpers");
const derivation_1 = require("@ledgerhq/ledger-wallet-framework/derivation");
const bignumber_js_1 = require("bignumber.js");
const invariant_1 = __importDefault(require("invariant"));
const config_1 = __importDefault(require("../config"));
const constants_1 = require("../constants");
const logic_1 = require("../logic");
const utils_1 = require("../logic/utils");
const api_1 = require("../network/api");
const thirdweb_1 = require("../network/thirdweb");
const utils_2 = require("../network/utils");
const utils_3 = require("./utils");
const getAccountShapeV2 = async ({ address, evmAddress, liveAccountId, currency, initialAccount, blacklistedTokenIds, }) => {
// get current account balance and tokens
// tokens are fetched with separate requests to get "created_timestamp" for each token
// based on this, ASSOCIATE_TOKEN operations can be connected with tokens
const [mirrorAccount, mirrorTokens, erc20Tokens] = await Promise.all([
api_1.apiClient.getAccount(address),
api_1.apiClient.getAccountTokens(address),
(0, utils_2.getERC20BalancesForAccountV2)(address),
]);
const accountBalance = new bignumber_js_1.BigNumber(mirrorAccount.balance.balance);
// we should sync again when new tokens are added or blacklist changes
const syncHash = await (0, account_1.getSyncHash)(currency.id, blacklistedTokenIds);
const shouldSyncFromScratch = !initialAccount || syncHash !== initialAccount?.syncHash;
const pendingOperations = shouldSyncFromScratch ? [] : (initialAccount?.pendingOperations ?? []);
const oldOperations = shouldSyncFromScratch ? [] : (initialAccount?.operations ?? []);
const latestOperation = oldOperations[0];
// grab latest operation timestamps for incremental sync
let latestOperationTimestamp = null;
if (!shouldSyncFromScratch && latestOperation) {
const timestamp = Math.floor(latestOperation.date.getTime() / 1000);
latestOperationTimestamp = new bignumber_js_1.BigNumber(timestamp).toFixed(9);
}
const latestAccountOperations = await (0, logic_1.listOperationsV2)({
currency,
address,
evmAddress,
mirrorTokens,
erc20Tokens,
...(latestOperationTimestamp && { cursor: latestOperationTimestamp }),
fetchAllPages: true,
skipFeesForTokenOperations: false,
useEncodedHash: true,
useSyntheticBlocks: false,
});
const newOperations = await (0, utils_3.prepareOperations)(latestAccountOperations.coinOperations, latestAccountOperations.tokenOperations);
const enrichedNewOperations = (0, utils_3.applyPendingExtras)(newOperations, pendingOperations);
const operations = shouldSyncFromScratch
? enrichedNewOperations
: (0, jsHelpers_1.mergeOps)(oldOperations, enrichedNewOperations);
const delegation = typeof mirrorAccount.staked_node_id === "number"
? {
nodeId: mirrorAccount.staked_node_id,
delegated: accountBalance,
pendingReward: new bignumber_js_1.BigNumber(mirrorAccount.pending_reward),
}
: null;
const newSubAccounts = await (0, utils_3.getSubAccounts)({
ledgerAccountId: liveAccountId,
latestTokenOperations: latestAccountOperations.tokenOperations,
mirrorTokens,
erc20Tokens,
});
const subAccounts = shouldSyncFromScratch
? newSubAccounts
: (0, utils_3.mergeSubAccounts)(initialAccount, newSubAccounts);
return {
id: liveAccountId,
freshAddress: address,
syncHash,
lastSyncDate: new Date(),
balance: accountBalance,
spendableBalance: accountBalance,
operations: operations,
operationsCount: operations.length,
// NOTE: there are no "blocks" in hedera
// set a value just so that operations are considered confirmed according to isConfirmedOperation
blockHeight: constants_1.HARDCODED_BLOCK_HEIGHT,
subAccounts,
hederaResources: {
maxAutomaticTokenAssociations: mirrorAccount.max_automatic_token_associations,
isAutoTokenAssociationEnabled: mirrorAccount.max_automatic_token_associations === -1,
delegation,
},
};
};
const getAccountShape = async (info, { blacklistedTokenIds }) => {
const { currency, derivationMode, address, initialAccount } = info;
(0, invariant_1.default)(address, "hedera: address is expected");
const evmAddress = await (0, utils_1.toEVMAddress)(address);
(0, invariant_1.default)(evmAddress, `hedera: evm address is missing for ${address}`);
const coinConfig = config_1.default.getCoinConfig(currency.id);
const liveAccountId = (0, account_1.encodeAccountId)({
type: "js",
version: "2",
currencyId: currency.id,
xpubOrAddress: address,
derivationMode,
});
if (coinConfig.useHgraphForErc20) {
return getAccountShapeV2({
address,
evmAddress,
liveAccountId,
currency,
initialAccount,
blacklistedTokenIds,
});
}
// get current account balance and tokens
// tokens are fetched with separate requests to get "created_timestamp" for each token
// based on this, ASSOCIATE_TOKEN operations can be connected with tokens
const [mirrorAccount, mirrorTokens, erc20TokenBalances] = await Promise.all([
api_1.apiClient.getAccount(address),
api_1.apiClient.getAccountTokens(address),
(0, utils_2.getERC20BalancesForAccount)(evmAddress),
]);
const accountBalance = new bignumber_js_1.BigNumber(mirrorAccount.balance.balance);
// we should sync again when new tokens are added or blacklist changes
const syncHash = await (0, account_1.getSyncHash)(currency.id, blacklistedTokenIds);
const shouldSyncFromScratch = !initialAccount || syncHash !== initialAccount?.syncHash;
const pendingOperations = shouldSyncFromScratch ? [] : (initialAccount?.pendingOperations ?? []);
const oldOperations = shouldSyncFromScratch ? [] : (initialAccount?.operations ?? []);
const oldERC20Operations = oldOperations.filter(item => item.standard === "erc20");
const latestOperation = oldOperations[0];
const erc20LatestOperation = oldERC20Operations[0];
const pendingOperationHashes = new Set(pendingOperations.map(op => op.hash));
const erc20OperationHashes = new Set(oldERC20Operations.map(op => op.hash));
// grab latest operation timestamps for incremental sync
let latestOperationTimestamp = null;
let erc20LatestOperationTimestamp = null;
if (!shouldSyncFromScratch && latestOperation) {
const timestamp = Math.floor(latestOperation.date.getTime() / 1000);
latestOperationTimestamp = new bignumber_js_1.BigNumber(timestamp).toString();
}
if (!shouldSyncFromScratch && erc20LatestOperation) {
const timestamp = Math.floor(erc20LatestOperation.date.getTime() / 1000);
erc20LatestOperationTimestamp = new bignumber_js_1.BigNumber(timestamp).toString();
}
const [latestAccountOperations, erc20Transactions] = await Promise.all([
(0, logic_1.listOperations)({
currency,
address,
mirrorTokens,
cursor: latestOperationTimestamp?.toString(),
fetchAllPages: true,
skipFeesForTokenOperations: false,
useEncodedHash: true,
useSyntheticBlocks: false,
}),
thirdweb_1.thirdwebClient.getERC20TransactionsForAccount({
address,
contractAddresses: erc20TokenBalances.map(token => token.token.contractAddress),
since: erc20LatestOperationTimestamp,
}),
]);
const newOperations = await (0, utils_3.prepareOperations)(latestAccountOperations.coinOperations, latestAccountOperations.tokenOperations);
const enrichedNewOperations = (0, utils_3.applyPendingExtras)(newOperations, pendingOperations);
const operations = shouldSyncFromScratch
? enrichedNewOperations
: (0, jsHelpers_1.mergeOps)(oldOperations, enrichedNewOperations);
const delegation = typeof mirrorAccount.staked_node_id === "number"
? {
nodeId: mirrorAccount.staked_node_id,
delegated: accountBalance,
pendingReward: new bignumber_js_1.BigNumber(mirrorAccount.pending_reward),
}
: null;
// how ERC20 operations are handled:
// - mirror node doesn't include "IN" erc20 token transactions
// - mirror node returns "CONTRACT_CALL" (OUT) erc20 token transactions made from given account address
// - mirror node doesn't return "CONTRACT_CALL" (OUT) erc20 token transactions made from 3rd party with allowance
//
// 1. mirror node transactions are already transformed into operations at this point + we have raw erc20 transactions fetched from thirdweb
// 2. related mirror node transaction must be fetched for each erc20 transaction (to get fee and consensus timestamp)
// 3. CONTRACTCALL operations must be removed if existing operations already include erc20 operation with the same tx.hash
// 4. ERC20 transactions must be classified into two groups: patchList and addList
// - patchList: transactions which are already present in mirror operations (we can have CONTRACT_CALL from mirror node that we can transform into "FEES")
// - addList should include transactions which are missing in mirror operations (e.g. "IN" erc20 token transaction and "OUT" made by 3rd party with allowance)
// 5. list of all operations must be updated based on prepared `patchList` and `addList`
// 6. sub accounts must get erc20 tokens and erc20 operations in addition to hts tokens and hts operations
// 7. postSync must remove pending operations that are already confirmed as erc20 operations
const { updatedOperations, newERC20TokenOperations } = await (0, utils_3.integrateERC20Operations)({
ledgerAccountId: liveAccountId,
address,
allOperations: operations,
latestERC20Transactions: erc20Transactions,
pendingOperationHashes,
erc20OperationHashes,
});
const newSubAccounts = await (0, utils_3.getSubAccounts)({
ledgerAccountId: liveAccountId,
latestTokenOperations: [...latestAccountOperations.tokenOperations, ...newERC20TokenOperations],
mirrorTokens,
erc20Tokens: erc20TokenBalances,
});
const subAccounts = shouldSyncFromScratch
? newSubAccounts
: (0, utils_3.mergeSubAccounts)(initialAccount, newSubAccounts);
return {
id: liveAccountId,
freshAddress: address,
syncHash,
lastSyncDate: new Date(),
balance: accountBalance,
spendableBalance: accountBalance,
operations: updatedOperations,
operationsCount: updatedOperations.length,
// NOTE: there are no "blocks" in hedera
// Set a value just so that operations are considered confirmed according to isConfirmedOperation
blockHeight: constants_1.HARDCODED_BLOCK_HEIGHT,
subAccounts,
hederaResources: {
maxAutomaticTokenAssociations: mirrorAccount.max_automatic_token_associations,
isAutoTokenAssociationEnabled: mirrorAccount.max_automatic_token_associations === -1,
delegation,
},
};
};
exports.getAccountShape = getAccountShape;
const buildIterateResult = async ({ result: rootResult }) => {
const mirrorAccounts = await api_1.apiClient.getAccountsForPublicKey(rootResult.publicKey);
const addresses = mirrorAccounts.map(a => a.account);
return async ({ currency, derivationMode, index }) => {
const derivationScheme = (0, derivation_1.getDerivationScheme)({
derivationMode,
currency,
});
const freshAddressPath = (0, derivation_1.runDerivationScheme)(derivationScheme, currency, {
account: index,
});
return addresses[index]
? {
address: addresses[index],
publicKey: addresses[index],
path: freshAddressPath,
}
: null;
};
};
exports.buildIterateResult = buildIterateResult;
// TODO: remove once migration to new API is complete
// it might be necessary to remove pending operations after ERC20 patching done by `integrateERC20Operations`
const postSync = (_initial, synced) => {
const coinConfig = config_1.default.getCoinConfig(synced.currency.id);
if (coinConfig.useHgraphForErc20) {
return synced;
}
const erc20Operations = synced.operations.filter(op => op.standard === "erc20");
const erc20Hashes = new Set(erc20Operations.map(op => op.hash));
const excludeConfirmedERC20Operations = (o) => !erc20Hashes.has(o.hash);
return {
...synced,
pendingOperations: synced.pendingOperations.filter(excludeConfirmedERC20Operations),
subAccounts: (synced.subAccounts ?? []).map(subAccount => {
return {
...subAccount,
pendingOperations: subAccount.pendingOperations.filter(excludeConfirmedERC20Operations),
};
}),
};
};
exports.postSync = postSync;
//# sourceMappingURL=synchronisation.js.map