@ledgerhq/coin-hedera
Version:
Ledger Hedera Coin integration
279 lines • 12.5 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.applyPendingExtras = exports.mergeSubAccounts = exports.prepareOperations = exports.getSubAccounts = exports.calculateAmount = void 0;
exports.patchOperationWithExtra = patchOperationWithExtra;
const state_1 = require("@ledgerhq/cryptoassets/state");
const account_1 = require("@ledgerhq/ledger-wallet-framework/account");
const jsHelpers_1 = require("@ledgerhq/ledger-wallet-framework/bridge/jsHelpers");
const operation_1 = require("@ledgerhq/ledger-wallet-framework/operation");
const bignumber_js_1 = __importDefault(require("bignumber.js"));
const constants_1 = require("../constants");
const estimateFees_1 = require("../logic/estimateFees");
const utils_1 = require("../logic/utils");
const estimateMaxSpendable_1 = require("./estimateMaxSpendable");
const calculateCoinAmount = async ({ account, transaction, operationType, }) => {
const estimatedFees = await (0, estimateFees_1.estimateFees)({ currencyId: account.currency.id, operationType });
const amount = transaction.useAllAmount
? await (0, estimateMaxSpendable_1.estimateMaxSpendable)({ account, transaction })
: transaction.amount;
return {
amount,
totalSpent: amount.plus(estimatedFees.tinybars),
};
};
const calculateTokenAmount = async ({ account, tokenAccount, transaction, }) => {
const amount = transaction.useAllAmount
? await (0, estimateMaxSpendable_1.estimateMaxSpendable)({ account: tokenAccount, parentAccount: account, transaction })
: transaction.amount;
return {
amount,
totalSpent: amount,
};
};
const calculateAmount = ({ account, transaction, }) => {
const subAccount = (0, account_1.findSubAccountById)(account, transaction?.subAccountId || "");
const isTokenTransaction = (0, account_1.isTokenAccount)(subAccount);
if (isTokenTransaction) {
return calculateTokenAmount({ account, tokenAccount: subAccount, transaction });
}
const operationType = (0, utils_1.isTokenAssociateTransaction)(transaction)
? constants_1.HEDERA_OPERATION_TYPES.TokenAssociate
: constants_1.HEDERA_OPERATION_TYPES.CryptoTransfer;
return calculateCoinAmount({ account, transaction, operationType });
};
exports.calculateAmount = calculateAmount;
const getSubAccounts = async ({ ledgerAccountId, latestTokenOperations, mirrorTokens, erc20Tokens, }) => {
// Creating a Map of Operations by TokenCurrencies in order to know which TokenAccounts should be synced as well
const operationsByToken = new Map();
const subAccounts = [];
for (const tokenOperation of latestTokenOperations) {
const { token } = await (0, account_1.decodeTokenAccountId)(tokenOperation.accountId);
if (!token)
continue;
const isTokenListedInCAL = await (0, state_1.getCryptoAssetsStore)().findTokenByAddressInCurrency(token.contractAddress, token.parentCurrencyId);
if (!isTokenListedInCAL)
continue;
if (!operationsByToken.has(token)) {
operationsByToken.set(token, []);
}
operationsByToken.get(token)?.push(tokenOperation);
}
// extract token accounts from existing operations
for (const [token, tokenOperations] of operationsByToken.entries()) {
const parentAccountId = ledgerAccountId;
let balance = null;
if (token.tokenType === "erc20") {
const rawBalance = erc20Tokens.find(t => t.token.contractAddress === token.contractAddress);
balance = rawBalance === undefined ? null : new bignumber_js_1.default(rawBalance.balance);
}
else {
const rawBalance = mirrorTokens.find(t => t.token_id === token.contractAddress)?.balance;
balance = rawBalance === undefined ? null : new bignumber_js_1.default(rawBalance);
}
if (!balance) {
continue;
}
subAccounts.push({
type: "TokenAccount",
id: (0, account_1.encodeTokenAccountId)(parentAccountId, token),
parentId: parentAccountId,
token,
balance,
spendableBalance: balance,
creationDate: tokenOperations.length > 0 ? tokenOperations[tokenOperations.length - 1].date : new Date(),
operations: tokenOperations,
operationsCount: tokenOperations.length,
pendingOperations: [],
balanceHistoryCache: account_1.emptyHistoryCache,
swapHistory: [],
});
}
// extract token accounts existing in the account's balance, but with no recorded operations yet
// e.g. hts tokens added via association flow, without any subsequent activity
// or erc20 tokens received from 3rd party
for (const rawToken of [...mirrorTokens, ...erc20Tokens]) {
const parentAccountId = ledgerAccountId;
const rawBalance = rawToken.balance;
const balance = new bignumber_js_1.default(rawBalance);
const isERC20 = "token" in rawToken;
const tokenAddress = isERC20 ? rawToken.token.contractAddress : rawToken.token_id;
const token = await (0, state_1.getCryptoAssetsStore)().findTokenByAddressInCurrency(tokenAddress, "hedera");
if (!token) {
continue;
}
const id = (0, account_1.encodeTokenAccountId)(parentAccountId, token);
const operations = operationsByToken.get(token) ?? [];
if (subAccounts.some(a => a.id === id)) {
continue;
}
if (isERC20 && operations.length === 0 && balance.isZero()) {
continue;
}
subAccounts.push({
type: "TokenAccount",
id,
parentId: parentAccountId,
token,
balance,
spendableBalance: balance,
creationDate: isERC20
? new Date()
: new Date(Number.parseFloat(rawToken.created_timestamp) * 1000),
operations: [],
operationsCount: 0,
pendingOperations: [],
balanceHistoryCache: account_1.emptyHistoryCache,
swapHistory: [],
});
}
return subAccounts;
};
exports.getSubAccounts = getSubAccounts;
// create NONE coin operation that will be a parent of an orphan child operation
const makeCoinOperationForOrphanChildOperation = async (childOperation) => {
const type = "NONE";
const { accountId } = await (0, account_1.decodeTokenAccountId)(childOperation.accountId);
const id = (0, operation_1.encodeOperationId)(accountId, childOperation.hash, type);
return {
id,
hash: childOperation.hash,
type,
value: new bignumber_js_1.default(0),
fee: new bignumber_js_1.default(0),
senders: [],
recipients: [],
blockHeight: childOperation.blockHeight,
blockHash: childOperation.blockHash,
transactionSequenceNumber: childOperation.transactionSequenceNumber,
subOperations: [],
nftOperations: [],
internalOperations: [],
accountId: "",
date: childOperation.date,
extra: {},
};
};
// this util handles:
// - linking sub operations with coin operations, e.g. token transfer with fee payment
// - if possible, assigning `extra.associatedTokenId = mirrorToken.tokenId` based on operation's consensus timestamp
const prepareOperations = async (coinOperations, tokenOperations) => {
const preparedCoinOperations = coinOperations.map(op => ({ ...op }));
const preparedTokenOperations = tokenOperations.map(op => ({ ...op }));
// loop through coin operations to prepare a map of hash => operations
const coinOperationsByHash = {};
preparedCoinOperations.forEach(op => {
if (!coinOperationsByHash[op.hash]) {
coinOperationsByHash[op.hash] = [];
}
op.subOperations = [];
coinOperationsByHash[op.hash].push(op);
});
// loop through token operations to potentially copy them as a child operation of a coin operation
for (const tokenOperation of preparedTokenOperations) {
const { token } = await (0, account_1.decodeTokenAccountId)(tokenOperation.accountId);
if (!token)
continue;
let mainOperations = coinOperationsByHash[tokenOperation.hash];
if (!mainOperations?.length) {
const noneOperation = await makeCoinOperationForOrphanChildOperation(tokenOperation);
mainOperations = [noneOperation];
preparedCoinOperations.push(noneOperation);
}
// ugly loop in loop but in theory, this can only be a 2 elements array maximum in the case of a self send
for (const mainOperation of mainOperations) {
mainOperation.subOperations.push(tokenOperation);
}
}
return preparedCoinOperations;
};
exports.prepareOperations = prepareOperations;
/**
* 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
*/
const mergeSubAccounts = (initialAccount, newSubAccounts) => {
const oldSubAccounts = initialAccount?.subAccounts;
if (!oldSubAccounts) {
return newSubAccounts;
}
// map of already existing sub accounts by id
const oldSubAccountsById = {};
for (const oldSubAccount of oldSubAccounts) {
oldSubAccountsById[oldSubAccount.id] = oldSubAccount;
}
// looping through new sub accounts to compare them with already existing ones
// already existing will be updated if necessary (see `updatableSubAccountProperties`)
// new sub accounts will be added/pushed after already existing
const newSubAccountsToAdd = [];
for (const newSubAccount of newSubAccounts) {
const duplicatedAccount = oldSubAccountsById[newSubAccount.id];
if (!duplicatedAccount) {
newSubAccountsToAdd.push(newSubAccount);
continue;
}
const updates = {};
for (const { name, isOps } of updatableSubAccountProperties) {
if (!isOps) {
if (newSubAccount[name] !== duplicatedAccount[name]) {
// @ts-expect-error - TypeScript assumes all possible types could be assigned here
updates[name] = newSubAccount[name];
}
}
else {
updates[name] = (0, jsHelpers_1.mergeOps)(duplicatedAccount[name], newSubAccount[name]);
}
}
// update the operationsCount in case the mergeOps changed it
updates.operationsCount =
updates.operations?.length || duplicatedAccount?.operations?.length || 0;
// modify the map with the updated sub account with a new ref
oldSubAccountsById[newSubAccount.id] = {
...duplicatedAccount,
...updates,
};
}
const updatedSubAccounts = Object.values(oldSubAccountsById);
return [...updatedSubAccounts, ...newSubAccountsToAdd];
};
exports.mergeSubAccounts = mergeSubAccounts;
const applyPendingExtras = (existing, pending) => {
const pendingOperationsByHash = new Map(pending.map(op => [op.hash, op]));
return existing.map(op => {
const pendingOp = pendingOperationsByHash.get(op.hash);
if (!pendingOp)
return op;
if (!(0, utils_1.isValidExtra)(op.extra))
return op;
if (!(0, utils_1.isValidExtra)(pendingOp.extra))
return op;
return {
...op,
extra: {
...pendingOp.extra,
...op.extra,
},
};
});
};
exports.applyPendingExtras = applyPendingExtras;
function patchOperationWithExtra(operation, extra) {
return {
...operation,
extra,
subOperations: (operation.subOperations ?? []).map(op => ({ ...op, extra })),
nftOperations: (operation.nftOperations ?? []).map(op => ({ ...op, extra })),
};
}
//# sourceMappingURL=utils.js.map