UNPKG

@ledgerhq/coin-hedera

Version:
272 lines 13.8 kB
"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