UNPKG

@ledgerhq/coin-canton

Version:
256 lines 11.8 kB
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