@ledgerhq/coin-aptos
Version:
Ledger Aptos Coin integration
207 lines • 8.99 kB
JavaScript
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