@ledgerhq/coin-canton
Version:
256 lines • 11.8 kB
JavaScript
import { getCryptoAssetsStore } from "@ledgerhq/ledger-wallet-framework/cryptoAssetsStore";
import { encodeAccountId } from "@ledgerhq/ledger-wallet-framework/account/index";
import { mergeOps } from "@ledgerhq/ledger-wallet-framework/bridge/jsHelpers";
import { encodeOperationId } from "@ledgerhq/ledger-wallet-framework/operation";
import BigNumber from "bignumber.js";
import { getBalance } from "../common-logic/account/getBalance";
import { determineOperationType } from "../common-logic/history/operationType";
import coinConfig from "../config";
import { isCantonAccountEmpty } from "../helpers";
import { getCalTokensCached, getEnabledInstrumentsCached, getKey, getLedgerEnd, getOperations, getPartyById, getPendingTransferProposals, SEPARATOR, } from "../network/gateway";
import resolver from "../signer";
import { buildSubAccounts } from "./buildSubAccounts";
import { isAccountOnboarded } from "./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 = determineOperationType(details?.operationType, txInfo.type, transferValue, senders, partyId);
let value = new BigNumber(transferValue);
if (type === "OUT" || type === "FEES") {
value = value.plus(fee);
}
const feeValue = new BigNumber(fee);
const memo = details?.metadata?.reason;
const op = {
id: 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(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));
};
export async function filterDisabledTokenAccounts(currency, subAccounts, calTokens) {
if (!subAccounts || subAccounts.length === 0) {
return [];
}
const enabledInstruments = await 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(getKey(instrumentId, adminId));
});
}
export 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 = resolver(signerContext);
const addressResult = await getAddress(info.deviceId ?? "", {
path: derivationPath,
currency: currency,
derivationMode: derivationMode,
verify: false,
});
publicKey = addressResult.publicKey;
const result = await 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 getPartyById(currency, xpubOrAddress);
if (public_key)
publicKey = public_key;
}
const accountId = encodeAccountId({
type: "js",
version: "2",
currencyId: currency.id,
xpubOrAddress: xpubOrAddress,
derivationMode,
});
const { nativeInstrumentId } = coinConfig.getCoinConfig(currency.id);
const balances = xpubOrAddress ? await getBalance(currency, xpubOrAddress) : [];
const pendingTransferProposals = xpubOrAddress
? await getPendingTransferProposals(currency, xpubOrAddress)
: [];
// Aggregate all balances by instrument (unlocked + locked)
const aggregatedBalances = new Map();
const proposalInstrumentKeys = new Set(pendingTransferProposals.map(proposal => getKey(proposal.instrument_id, proposal.instrument_admin)));
for (const key of proposalInstrumentKeys) {
if (aggregatedBalances.has(key))
continue;
const [instrumentId, adminId] = key.split(SEPARATOR);
balances.push({
value: 0n,
locked: 0n,
utxoCount: 0,
instrumentId,
adminId,
asset: { type: "token", assetReference: instrumentId },
});
}
const calTokens = await 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 getCryptoAssetsStore().findTokenById(tokenId);
if (!token)
continue;
tokensByKey.set(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
: 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((nativeBalance?.unlockedBalance ?? 0n).toString());
const lockedAmount = new BigNumber((nativeBalance?.lockedBalance ?? 0n).toString());
const totalBalance = unlockedAmount.plus(lockedAmount);
const reserveMin = new BigNumber(coinConfig.getCoinConfig(currency.id).minReserve || 0);
const spendableBalance = BigNumber.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 getOperations(currency, xpubOrAddress, {
cursor: startAt,
limit: 100,
});
const newOperations = filterOperations(transactionData.operations, accountId, xpubOrAddress, pendingTransferProposals);
operations = 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 = 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 = !isCantonAccountEmpty({
operationsCount: mainAccountOperations.length,
balance: totalBalance,
subAccounts: filteredSubAccounts,
cantonResources,
});
const blockHeight = await 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