@ledgerhq/coin-stacks
Version:
Ledger Stacks Coin integration
160 lines • 7.36 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.sync = exports.getAccountShape = void 0;
exports.calculateSpendableBalance = calculateSpendableBalance;
exports.createTokenAccount = createTokenAccount;
exports.buildTokenAccounts = buildTokenAccounts;
const index_1 = require("@ledgerhq/coin-framework/account/index");
const jsHelpers_1 = require("@ledgerhq/coin-framework/bridge/jsHelpers");
const transactions_1 = require("@stacks/transactions");
const bignumber_js_1 = __importDefault(require("bignumber.js"));
const invariant_1 = __importDefault(require("invariant"));
const api_1 = require("../network/api");
const misc_1 = require("./utils/misc");
const logs_1 = require("@ledgerhq/logs");
const state_1 = require("@ledgerhq/cryptoassets/state");
/**
* Calculates the spendable balance by subtracting pending transactions from the total balance
*/
function calculateSpendableBalance(totalBalance, pendingTxs) {
let spendableBalance = totalBalance;
for (const tx of pendingTxs) {
spendableBalance = spendableBalance
.minus(new bignumber_js_1.default(tx.fee_rate))
.minus(new bignumber_js_1.default(tx.token_transfer.amount));
}
return spendableBalance;
}
/**
* Creates a token account for a specific token
*/
async function createTokenAccount(address, parentAccountId, tokenId, tokenBalance, transactionsList, initialAccount) {
try {
const token = await (0, state_1.getCryptoAssetsStore)().findTokenByAddressInCurrency(tokenId, "stacks");
if (!tokenId || !token) {
(0, logs_1.log)("error", `stacks token not found, addr: ${tokenId}`);
return null;
}
const bnBalance = new bignumber_js_1.default(tokenBalance || "0");
const tokenAccountId = (0, index_1.encodeTokenAccountId)(parentAccountId, token);
// Process operations for this token
const operations = transactionsList
.flatMap(txn => (0, misc_1.sip010TxnToOperation)(txn, address, tokenAccountId))
.flat()
.sort((a, b) => b.date.getTime() - a.date.getTime());
// Skip empty accounts with zero balance and no operations
if (operations.length === 0 && bnBalance.isZero()) {
return null;
}
// Preserve existing pending operations if available
const maybeExistingSubAccount = initialAccount?.subAccounts?.find(a => a.id === tokenAccountId);
const tokenAccount = {
type: "TokenAccount",
id: tokenAccountId,
parentId: parentAccountId,
token,
balance: bnBalance,
spendableBalance: bnBalance,
operationsCount: operations.length,
operations,
pendingOperations: maybeExistingSubAccount?.pendingOperations ?? [],
creationDate: operations.length > 0 ? operations[operations.length - 1].date : new Date(),
swapHistory: maybeExistingSubAccount?.swapHistory ?? [],
balanceHistoryCache: index_1.emptyHistoryCache, // calculated in the jsHelpers
};
return tokenAccount;
}
catch (e) {
(0, logs_1.log)("error", "stacks error creating token account", e);
return null;
}
}
/**
* Builds token accounts for all tokens with transactions or balances
*/
async function buildTokenAccounts(address, parentAccountId, tokenTxs, tokenBalances, initialAccount) {
try {
const tokenAccounts = [];
// Process all tokens that have transactions
for (const [tokenId, transactions] of Object.entries(tokenTxs)) {
const balance = tokenBalances[tokenId] || "0";
const tokenAccount = await createTokenAccount(address, parentAccountId, tokenId, balance, transactions, initialAccount);
if (tokenAccount) {
tokenAccounts.push(tokenAccount);
}
}
// Process any tokens with balances but no transactions
for (const [tokenId, balance] of Object.entries(tokenBalances)) {
// Skip tokens we've already processed
if (tokenTxs[tokenId])
continue;
// Skip zero balances
if (new bignumber_js_1.default(balance).isZero())
continue;
const tokenAccount = await createTokenAccount(address, parentAccountId, tokenId, balance, [], // No transactions
initialAccount);
if (tokenAccount) {
tokenAccounts.push(tokenAccount);
}
}
return tokenAccounts;
}
catch (e) {
(0, logs_1.log)("error", "stacks error building token accounts", e);
return [];
}
}
const getAccountShape = async (info) => {
const { initialAccount, currency, rest = {}, derivationMode } = info;
// for bridge tests specifically the `rest` object is empty and therefore the publicKey is undefined
// reconciliatePublicKey tries to get pubKey from rest object and then from accountId
const pubKey = (0, misc_1.reconciliatePublicKey)(rest.publicKey, initialAccount);
(0, invariant_1.default)(pubKey, "publicKey is required");
const accountId = (0, index_1.encodeAccountId)({
type: "js",
version: "2",
currencyId: currency.id,
xpubOrAddress: pubKey,
derivationMode,
});
const address = (0, transactions_1.getAddressFromPublicKey)(pubKey);
// Make API calls in parallel for better performance
const [blockHeight, balanceResp, txsResult, tokenBalances, mempoolTxs] = await Promise.all([
(0, api_1.fetchBlockHeight)(),
(0, api_1.fetchBalances)(address),
(0, api_1.fetchFullTxs)(address),
(0, api_1.fetchAllTokenBalances)(address),
(0, api_1.fetchFullMempoolTxs)(address),
]);
const [rawTxs, tokenTxs] = txsResult;
const balance = new bignumber_js_1.default(balanceResp.balance);
// Calculate spendable balance by considering pending transactions
const spendableBalance = calculateSpendableBalance(balance, mempoolTxs);
// Process pending operations
const pendingOperations = mempoolTxs.flatMap((0, misc_1.mapPendingTxToOps)(accountId, address));
// Process operations from confirmed transactions
const operations = pendingOperations.concat(rawTxs.flatMap((0, misc_1.mapTxToOps)(accountId, address)));
// Build token sub-accounts
const tokenAccounts = await buildTokenAccounts(address, accountId, tokenTxs, tokenBalances, initialAccount);
const result = {
id: accountId,
subAccounts: tokenAccounts,
xpub: pubKey,
freshAddress: address,
balance,
spendableBalance,
// merge operations from both token and account
operations: [
...operations,
...tokenAccounts.flatMap(t => (0, misc_1.sip010OpToParentOp)(t.operations, accountId)),
].sort((a, b) => b.date.getTime() - a.date.getTime()),
blockHeight: blockHeight.chain_tip.block_height,
};
return result;
};
exports.getAccountShape = getAccountShape;
exports.sync = (0, jsHelpers_1.makeSync)({ getAccountShape: exports.getAccountShape });
//# sourceMappingURL=synchronization.js.map