@ledgerhq/coin-canton
Version:
263 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.filterDisabledTokenAccounts = filterDisabledTokenAccounts;
exports.makeGetAccountShape = makeGetAccountShape;
const cryptoAssetsStore_1 = require("@ledgerhq/ledger-wallet-framework/cryptoAssetsStore");
const index_1 = require("@ledgerhq/ledger-wallet-framework/account/index");
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 getBalance_1 = require("../common-logic/account/getBalance");
const operationType_1 = require("../common-logic/history/operationType");
const config_1 = __importDefault(require("../config"));
const helpers_1 = require("../helpers");
const gateway_1 = require("../network/gateway");
const signer_1 = __importDefault(require("../signer"));
const buildSubAccounts_1 = require("./buildSubAccounts");
const onboard_1 = require("./onboard");
function isValidOperation(txInfo) {
return (txInfo.asset !== undefined &&
(txInfo.asset.type === "native" || txInfo.asset.type === "token") &&
typeof txInfo.asset.instrumentId === "string" &&
typeof txInfo.asset.instrumentAdmin === "string" &&
txInfo.transfers !== undefined &&
txInfo.transfers.length > 0 &&
txInfo.senders !== undefined &&
txInfo.recipients !== undefined);
}
const txInfoToOperationAdapter = (accountId, partyId) => (txInfo) => {
const { asset: { instrumentId, instrumentAdmin }, transaction_hash, uid, block: { height, hash }, senders, recipients, transaction_timestamp, fee: { value: fee }, transfers: [{ value: transferValue, details }], } = txInfo;
const type = (0, operationType_1.determineOperationType)(details?.operationType, txInfo.type, transferValue, senders, partyId);
let value = new bignumber_js_1.default(transferValue);
if (type === "OUT" || type === "FEES") {
value = value.plus(fee);
}
const feeValue = new bignumber_js_1.default(fee);
const memo = details?.metadata?.reason;
const op = {
id: (0, operation_1.encodeOperationId)(accountId, transaction_hash, type),
hash: transaction_hash,
accountId,
type,
value,
fee: feeValue,
blockHash: hash ?? "",
blockHeight: height,
senders,
recipients,
date: new Date(transaction_timestamp),
transactionSequenceNumber: new bignumber_js_1.default(height),
extra: {
uid,
memo,
instrumentId,
instrumentAdmin,
},
};
return op;
};
const filterOperations = (transactions, accountId, partyId, pendingTransferProposals) => {
const pendingUpdateIds = new Set(pendingTransferProposals.map(p => p.update_id));
return transactions
.filter(txInfo => !pendingUpdateIds.has(txInfo.transaction_hash))
.filter(isValidOperation)
.map(txInfoToOperationAdapter(accountId, partyId));
};
async function filterDisabledTokenAccounts(currency, subAccounts, calTokens) {
if (!subAccounts || subAccounts.length === 0) {
return [];
}
const enabledInstruments = await (0, gateway_1.getEnabledInstrumentsCached)(currency);
return subAccounts.filter(subAccount => {
const instrumentId = calTokens.get(subAccount.token.id);
const adminId = subAccount.token.contractAddress;
if (!instrumentId || !adminId) {
return false;
}
return enabledInstruments.has((0, gateway_1.getKey)(instrumentId, adminId));
});
}
function makeGetAccountShape(signerContext) {
return async (info) => {
const { address, currency, derivationMode, derivationPath, initialAccount } = info;
let isOnboarded = initialAccount?.cantonResources?.isOnboarded ?? false;
let xpubOrAddress = (initialAccount?.xpub || initialAccount?.cantonResources?.xpub) ?? "";
let publicKey = initialAccount?.cantonResources?.publicKey;
if (!xpubOrAddress && !publicKey) {
const getAddress = (0, signer_1.default)(signerContext);
const addressResult = await getAddress(info.deviceId ?? "", {
path: derivationPath,
currency: currency,
derivationMode: derivationMode,
verify: false,
});
publicKey = addressResult.publicKey;
const result = await (0, onboard_1.isAccountOnboarded)(currency, publicKey);
isOnboarded = result.isOnboarded;
if (isOnboarded && result.partyId) {
xpubOrAddress = result.partyId;
}
}
// Backfill publicKey for an already-onboarded account (xpub present but no
// publicKey — e.g. synced before publicKey was captured). Deviceless: the
// gateway party lookup returns it. Without publicKey, validateTopology can't
// run and the topology-change prompt never shows. LIVE-34585
if (xpubOrAddress && !publicKey) {
const { public_key } = await (0, gateway_1.getPartyById)(currency, xpubOrAddress);
if (public_key)
publicKey = public_key;
}
const accountId = (0, index_1.encodeAccountId)({
type: "js",
version: "2",
currencyId: currency.id,
xpubOrAddress: xpubOrAddress,
derivationMode,
});
const { nativeInstrumentId } = config_1.default.getCoinConfig(currency.id);
const balances = xpubOrAddress ? await (0, getBalance_1.getBalance)(currency, xpubOrAddress) : [];
const pendingTransferProposals = xpubOrAddress
? await (0, gateway_1.getPendingTransferProposals)(currency, xpubOrAddress)
: [];
// Aggregate all balances by instrument (unlocked + locked)
const aggregatedBalances = new Map();
const proposalInstrumentKeys = new Set(pendingTransferProposals.map(proposal => (0, gateway_1.getKey)(proposal.instrument_id, proposal.instrument_admin)));
for (const key of proposalInstrumentKeys) {
if (aggregatedBalances.has(key))
continue;
const [instrumentId, adminId] = key.split(gateway_1.SEPARATOR);
balances.push({
value: 0n,
locked: 0n,
utxoCount: 0,
instrumentId,
adminId,
asset: { type: "token", assetReference: instrumentId },
});
}
const calTokens = await (0, gateway_1.getCalTokensCached)(currency);
const tokenIdentifierToId = new Map();
for (const [tokenId, tokenIdentifier] of calTokens.entries()) {
tokenIdentifierToId.set(tokenIdentifier, tokenId);
}
const tokensByKey = new Map();
for await (const balance of balances) {
const tokenId = tokenIdentifierToId.get(balance.instrumentId) ?? "";
const token = await (0, cryptoAssetsStore_1.getCryptoAssetsStore)().findTokenById(tokenId);
if (!token)
continue;
tokensByKey.set((0, gateway_1.getKey)(balance.instrumentId, balance.adminId), token);
}
for await (const balance of balances) {
const isNative = balance.instrumentId === nativeInstrumentId;
// Use just instrumentId for native (no admin), composite key for tokens
const balanceKey = isNative
? nativeInstrumentId
: (0, gateway_1.getKey)(balance.instrumentId, balance.adminId);
const token = isNative ? null : (tokensByKey.get(balanceKey) ?? null);
const existing = aggregatedBalances.get(balanceKey);
if (existing) {
if (balance.locked) {
existing.lockedBalance += balance.value;
}
else {
existing.unlockedBalance += balance.value;
}
existing.utxoCount += balance.utxoCount;
}
else {
aggregatedBalances.set(balanceKey, {
unlockedBalance: balance.locked ? 0n : balance.value,
lockedBalance: balance.locked ? balance.value : 0n,
utxoCount: balance.utxoCount,
token,
adminId: balance.adminId,
});
}
}
// Find native balance (token is null for native)
const nativeBalance = Array.from(aggregatedBalances.values()).find(data => data.token === null);
const unlockedAmount = new bignumber_js_1.default((nativeBalance?.unlockedBalance ?? 0n).toString());
const lockedAmount = new bignumber_js_1.default((nativeBalance?.lockedBalance ?? 0n).toString());
const totalBalance = unlockedAmount.plus(lockedAmount);
const reserveMin = new bignumber_js_1.default(config_1.default.getCoinConfig(currency.id).minReserve || 0);
const spendableBalance = bignumber_js_1.default.max(0, unlockedAmount.minus(reserveMin));
const instrumentUtxoCounts = {};
for (const [key, data] of aggregatedBalances) {
instrumentUtxoCounts[key] = data.utxoCount;
}
const tokenBalances = Array.from(aggregatedBalances.entries())
.filter(([, data]) => data.token !== null)
.map(([, { unlockedBalance, lockedBalance, token, adminId }]) => ({
totalBalance: unlockedBalance + lockedBalance,
spendableBalance: unlockedBalance,
token: token,
adminId,
}));
let operations = [];
if (xpubOrAddress) {
const oldOperations = initialAccount?.operations || [];
const startAt = oldOperations.length ? (oldOperations[0].blockHeight || 0) + 1 : 0;
const transactionData = await (0, gateway_1.getOperations)(currency, xpubOrAddress, {
cursor: startAt,
limit: 100,
});
const newOperations = filterOperations(transactionData.operations, accountId, xpubOrAddress, pendingTransferProposals);
operations = (0, jsHelpers_1.mergeOps)(oldOperations, newOperations);
}
// Filter main account operations (native instrument only)
const mainAccountOperations = operations.filter(op => {
const extra = op.extra;
return extra?.instrumentId === nativeInstrumentId;
});
// Build sub-accounts for tokens with their filtered operations
const subAccounts = (0, buildSubAccounts_1.buildSubAccounts)({
accountId,
tokenBalances,
existingSubAccounts: initialAccount?.subAccounts ?? [],
allOperations: operations,
pendingTransferProposals,
calTokens,
});
const cantonResources = {
isOnboarded,
instrumentUtxoCounts,
pendingTransferProposals,
...(publicKey ? { publicKey } : {}),
xpub: xpubOrAddress,
};
const filteredSubAccounts = await filterDisabledTokenAccounts(currency, subAccounts, calTokens);
const used = !(0, helpers_1.isCantonAccountEmpty)({
operationsCount: mainAccountOperations.length,
balance: totalBalance,
subAccounts: filteredSubAccounts,
cantonResources,
});
const blockHeight = await (0, gateway_1.getLedgerEnd)(currency);
const creationDate = mainAccountOperations.length > 0
? new Date(Math.min(...mainAccountOperations.map(op => op.date.getTime())))
: new Date();
const shape = {
id: accountId,
type: "Account",
balance: totalBalance,
blockHeight,
creationDate,
lastSyncDate: new Date(),
freshAddress: address,
seedIdentifier: address,
operations: mainAccountOperations,
operationsCount: mainAccountOperations.length,
spendableBalance,
subAccounts: filteredSubAccounts,
xpub: xpubOrAddress,
used,
cantonResources,
};
return shape;
};
}
//# sourceMappingURL=sync.js.map