UNPKG

@ledgerhq/coin-aptos

Version:
207 lines 8.99 kB
import { inferSubOperations } from "@ledgerhq/coin-framework/serialization/index"; import { decodeAccountId, encodeAccountId } from "@ledgerhq/coin-framework/account"; import { mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers"; import { AptosAPI } from "../network"; import { txsToOps } from "./logic"; import { decodeTokenAccountId, emptyHistoryCache, encodeTokenAccountId, } from "@ledgerhq/coin-framework/account/index"; import BigNumber from "bignumber.js"; import { getEnv } from "@ledgerhq/live-env"; /** * List of properties of a sub account that can be updated when 2 "identical" accounts are found */ const updatableSubAccountProperties = [ { name: "balance", isOps: false }, { name: "spendableBalance", isOps: false }, { name: "balanceHistoryCache", isOps: false }, { name: "operations", isOps: true }, { name: "pendingOperations", isOps: true }, ]; /** * In charge of smartly merging sub accounts while maintaining references as much as possible */ export const mergeSubAccounts = (initialAccount, newSubAccounts) => { const oldSubAccounts = initialAccount?.subAccounts; if (!oldSubAccounts) { return newSubAccounts; } // Creating a map of already existing sub accounts by id const oldSubAccountsById = {}; for (const oldSubAccount of oldSubAccounts) { oldSubAccountsById[oldSubAccount.id] = oldSubAccount; } // Looping on new sub accounts to compare them with already existing ones // Already existing will be updated if necessary (see `updatableSubAccountProperties`) // Fresh new sub accounts will be added/pushed after already existing const newSubAccountsToAdd = []; for (const newSubAccount of newSubAccounts) { const duplicatedAccount = oldSubAccountsById[newSubAccount.id]; // If this sub account was not already in the initialAccount if (!duplicatedAccount) { // We'll add it later newSubAccountsToAdd.push(newSubAccount); continue; } const updates = {}; for (const { name, isOps } of updatableSubAccountProperties) { if (!isOps) { if (newSubAccount[name] !== duplicatedAccount[name]) { updates[name] = newSubAccount[name]; } } else { updates[name] = mergeOps(duplicatedAccount[name], newSubAccount[name]) ?? []; } } // Updating the operationsCount in case the mergeOps changed it updates.operationsCount = updates.operations?.length || duplicatedAccount?.operations?.length || 0; // Modifying the Map with the updated sub account with a new ref oldSubAccountsById[newSubAccount.id] = { ...duplicatedAccount, ...updates, }; } const updatedSubAccounts = Object.values(oldSubAccountsById); return [...updatedSubAccounts, ...newSubAccountsToAdd]; }; /** * Fetch the balance for a token and creates a TokenAccount based on this and the provided operations */ export const getSubAccountShape = async (currency, address, parentId, token, operations) => { const aptosClient = new AptosAPI(currency.id); const tokenAccountId = encodeTokenAccountId(parentId, token); const balances = await aptosClient.getBalances(address, token.contractAddress); const balance = balances.length > 0 ? balances[0].amount : BigNumber(0); const firstOperation = operations .sort((a, b) => b.date.getTime() - a.date.getTime()) .at(operations.length - 1); return { type: "TokenAccount", id: tokenAccountId, parentId, token, balance, spendableBalance: balance, creationDate: firstOperation?.date || new Date(0), operations, operationsCount: operations.length, pendingOperations: [], balanceHistoryCache: emptyHistoryCache, swapHistory: [], }; }; /** * Getting all token related operations in order to provide TokenAccounts */ export const getSubAccounts = async (infos, address, accountId, lastTokenOperations) => { const { currency } = infos; // Creating a Map of Operations by TokenCurrencies in order to know which TokenAccounts should be synced as well const operationsByToken = new Map(); for (const operation of lastTokenOperations) { const { token } = await decodeTokenAccountId(operation.accountId); if (!token) continue; // TODO: do we need to check blacklistedTokenIds if (!operationsByToken.has(token)) { operationsByToken.set(token, []); } operationsByToken.get(token)?.push(operation); } // Fetching all TokenAccounts possible and providing already filtered operations const subAccountsPromises = []; for (const [token, ops] of operationsByToken.entries()) { subAccountsPromises.push(getSubAccountShape(currency, address, accountId, token, ops)); } return Promise.all(subAccountsPromises); }; export const getAccountShape = async (info) => { const { address, initialAccount, currency, derivationMode, rest } = info; const publicKey = rest?.publicKey || (initialAccount && decodeAccountId(initialAccount.id).xpubOrAddress); const accountId = encodeAccountId({ type: "js", version: "2", currencyId: currency.id, xpubOrAddress: publicKey || address, derivationMode, }); // "xpub" field is used to store publicKey to simulate transaction during sending tokens. // We can't get access to the Nano X via bluetooth on the step of simulation // but we need public key to simulate transaction. // "xpub" field is used because this field exists in ledger operation type const xpub = initialAccount?.xpub || publicKey || ""; const oldOperations = initialAccount?.operations || []; const aptosClient = new AptosAPI(currency.id); const { balance, transactions, blockHeight } = await aptosClient.getAccountInfo(address); const [newOperations, tokenOperations, stakingOperations] = await txsToOps(info, accountId, transactions); const operations = mergeOps(oldOperations, newOperations); const newSubAccounts = await getSubAccounts(info, address, accountId, tokenOperations); const shouldSyncFromScratch = initialAccount === undefined; const subAccounts = shouldSyncFromScratch ? newSubAccounts : mergeSubAccounts(initialAccount, newSubAccounts); operations?.forEach(op => { const subOperations = inferSubOperations(op.hash, subAccounts); op.subOperations = subOperations.length === 1 ? subOperations : subOperations.filter(op => !!op.blockHash); }); const stakingPositions = []; let activeBalance = BigNumber(0); let inactiveBalance = BigNumber(0); let pendingInactiveBalance = BigNumber(0); if (getEnv("APTOS_ENABLE_STAKING") === true) { const stakingPoolAddresses = getStakingPoolAddresses(stakingOperations); for (const stakingPoolAddress of stakingPoolAddresses) { const [active_string, inactive_string, pending_inactive_string] = await aptosClient.getDelegatorBalanceInPool(stakingPoolAddress, address); const active = BigNumber(active_string); const inactive = BigNumber(inactive_string); const pendingInactive = BigNumber(pending_inactive_string); stakingPositions.push({ active, inactive, pendingInactive, validatorId: stakingPoolAddress, }); activeBalance = activeBalance.plus(active); inactiveBalance = inactiveBalance.plus(inactive); pendingInactiveBalance = pendingInactiveBalance.plus(pendingInactive); } } const aptosResources = { activeBalance, inactiveBalance, pendingInactiveBalance, stakingPositions, }; const shape = { type: "Account", id: accountId, xpub, balance: balance .plus(aptosResources.activeBalance) .plus(aptosResources.pendingInactiveBalance) .plus(aptosResources.inactiveBalance), spendableBalance: balance, operations, operationsCount: operations.length, blockHeight, lastSyncDate: new Date(), subAccounts, aptosResources, }; return shape; }; export const getStakingPoolAddresses = (stakingOperations) => { const stakingPoolsAddrs = []; for (const op of stakingOperations) { if (!op.recipients.length) continue; const poolAddress = op.recipients[0]; if (poolAddress === "0x1") continue; if (!stakingPoolsAddrs.includes(poolAddress)) stakingPoolsAddrs.push(poolAddress); } return stakingPoolsAddrs; }; //# sourceMappingURL=synchronisation.js.map