UNPKG

@ledgerhq/live-common

Version:
290 lines • 14.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.genericGetAccountShape = genericGetAccountShape; 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 groupBy_1 = __importDefault(require("lodash/groupBy")); const alpaca_1 = require("./alpaca"); const bridge_1 = require("./bridge"); const utils_1 = require("./utils"); const serialization_1 = require("@ledgerhq/ledger-wallet-framework/serialization"); const buildSubAccounts_1 = require("./buildSubAccounts"); function isNftCoreOp(operation) { return (typeof operation.details?.ledgerOpType === "string" && ["NFT_IN", "NFT_OUT"].includes(operation.details?.ledgerOpType)); } function isIncomingCoreOp(operation) { const type = typeof operation.details?.ledgerOpType === "string" ? operation.details.ledgerOpType : operation.type; return type === "IN"; } function isInternalLiveOp(operation) { return !!operation.extra?.internal; } /** True when the op is a main-account (native) op, not a token/sub-account op */ function isNativeLiveOp(operation) { const assetReference = operation.extra?.assetReference; const assetOwner = operation.extra?.assetOwner; const hasAssetReference = typeof assetReference === "string" && assetReference.length > 0; const hasAssetOwner = typeof assetOwner === "string" && assetOwner.length > 0; // Native ops are those that do not have a non-empty asset reference/owner return !(hasAssetReference || hasAssetOwner); } /** * Parent recipients for token-only ops: use the token contract (assetReference), not the token transfer recipient. */ function getTokenContract(op) { const ref = op.extra?.assetReference; return typeof ref === "string" && ref.length > 0 ? ref : undefined; } /** Get the fee payer for this tx from the op (from API/extra). */ function getFeePayer(op) { const fp = op.extra?.feePayer; return typeof fp === "string" && fp.length > 0 ? fp : undefined; } /** Compare two addresses for equality, ignoring case. */ function isSameAddress(a, b) { return a.toLowerCase() === b.toLowerCase(); } /** True when the native op is outbound with value equal to fee (fees-only). */ function isFeesOnlyNativeOp(op) { return op.type === "OUT" && op.value !== null && op.fee != null && op.value.eq(op.fee); } /** Emit one parent op per native op: FEES when fees-only, otherwise passthrough. */ function parentOpsFromNativeOps(nativeOps, accountId, subOperations, internalOperations) { const out = []; for (const nativeOp of nativeOps) { // Native outgoing operation with value 0 (only fees) => output as single FEES op if (isFeesOnlyNativeOp(nativeOp)) { out.push((0, utils_1.cleanedOperation)({ id: (0, operation_1.encodeOperationId)(accountId, nativeOp.hash, "FEES"), hash: nativeOp.hash, accountId, type: "FEES", value: nativeOp.fee, fee: nativeOp.fee, blockHash: nativeOp.blockHash, blockHeight: nativeOp.blockHeight, senders: nativeOp.senders, recipients: nativeOp.recipients, date: nativeOp.date, transactionSequenceNumber: nativeOp.transactionSequenceNumber, hasFailed: nativeOp.hasFailed, extra: nativeOp.extra, subOperations, internalOperations, })); } // Otherwise, don't transform the operation else { out.push((0, utils_1.cleanedOperation)({ ...nativeOp, subOperations, internalOperations, })); } } return out; } /** One synthetic FEES or NONE parent when the tx has no native ops (e.g. token-only). */ function syntheticParentForTokenOnlyTx(referenceOp, accountId, address, subOperations, internalOperations) { // Parent operation is of type FEES if account has paid fees for the transaction, NONE otherwise. const feePayer = getFeePayer(referenceOp); const isFeePayer = feePayer !== undefined && isSameAddress(address, feePayer); const parentType = isFeePayer ? "FEES" : "NONE"; const parentValue = isFeePayer ? referenceOp.fee : new bignumber_js_1.default(0); // In the case of smart contract interaction, the contract must be the recipient of the parent operation => this // is why we need to extract this information from the operation details. const contract = getTokenContract(referenceOp); const parentRecipients = contract !== undefined ? [contract] : referenceOp.recipients ?? []; const parentSenders = referenceOp.senders ?? []; return (0, utils_1.cleanedOperation)({ id: (0, operation_1.encodeOperationId)(accountId, referenceOp.hash, parentType), hash: referenceOp.hash, accountId, type: parentType, value: parentValue, fee: referenceOp.fee, blockHash: referenceOp.blockHash, blockHeight: referenceOp.blockHeight, senders: parentSenders, recipients: parentRecipients, date: referenceOp.date, transactionSequenceNumber: referenceOp.transactionSequenceNumber, hasFailed: referenceOp.hasFailed, extra: referenceOp.extra, subOperations, internalOperations, }); } /** Parent op(s) for a tx that has non-internal ops (native and/or token). */ function parentOpsForTxWithNonInternalOperations(hash, transactionOps, internalOperations, newSubAccounts, accountId, address) { const nativeOps = transactionOps.filter(isNativeLiveOp); // inferSubOperations returns types-live Operation[]; we use OperationCommon in this bridge // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- framework type vs bridge type const subOperations = (0, serialization_1.inferSubOperations)(hash, newSubAccounts); // If transaction has native ops, use them as parents if (nativeOps.length > 0) return parentOpsFromNativeOps(nativeOps, accountId, subOperations, internalOperations); // If transaction has no native ops, create a synthetic parent const firstOp = transactionOps[0]; return [ syntheticParentForTokenOnlyTx(firstOp, accountId, address, subOperations, internalOperations), ]; } /** * Parent + internal ops for a tx that has only internal ops (e.g. contract transfer from B to C). * This case happens when an address A calls a smart contract, that performs a transfer from B to C, seen from B or * C's perspective. In this case, the parent operation must be of type NONE, with A as the sender and the contract * as the recipient => the sender of the internal operation is used as the recipient of the synthetic parent operation. */ function parentOpsForTxWithOnlyInternalOperations(hash, internalOperations, newSubAccounts, accountId) { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- framework type vs bridge type const subOperations = (0, serialization_1.inferSubOperations)(hash, newSubAccounts); const firstInternal = internalOperations[0]; if (!firstInternal) return []; const out = []; const feePayer = getFeePayer(firstInternal); if (feePayer != null) { out.push((0, utils_1.cleanedOperation)({ id: (0, operation_1.encodeOperationId)(accountId, hash, "NONE"), hash, accountId, type: "NONE", value: new bignumber_js_1.default(0), fee: firstInternal.fee, blockHash: firstInternal.blockHash, blockHeight: firstInternal.blockHeight, senders: [feePayer], recipients: firstInternal.senders, date: firstInternal.date, transactionSequenceNumber: firstInternal.transactionSequenceNumber, hasFailed: firstInternal.hasFailed, extra: firstInternal.extra, subOperations, internalOperations, })); } for (const internalOp of internalOperations) { out.push((0, utils_1.cleanedOperation)({ ...internalOp, subOperations, internalOperations, })); } return out; } /** * Emit parent operations per tx hash so the account has one top-level operation per transaction for normal transactions, * and two for self-sends (IN + OUT) or internal-only (NONE + IN). */ function buildParentOperations(newSubAccounts, newNonInternalOperations, newInternalOperations, accountId, address) { const nonInternalByHash = (0, groupBy_1.default)(newNonInternalOperations, "hash"); const internalByHash = (0, groupBy_1.default)(newInternalOperations, "hash"); const result = []; // Inspect non-internal ops first to create parent ops for (const [hash, transactionOps] of Object.entries(nonInternalByHash)) { const internalOperations = internalByHash[hash] ?? []; result.push(...parentOpsForTxWithNonInternalOperations(hash, transactionOps, internalOperations, newSubAccounts, accountId, address)); } // If transaction only has internal ops, we must create a synthetic parent op as well for (const [hash, internalOperations] of Object.entries(internalByHash)) { if (hash in nonInternalByHash) continue; result.push(...parentOpsForTxWithOnlyInternalOperations(hash, internalOperations, newSubAccounts, accountId)); } return result; } function genericGetAccountShape(network, kind) { return async (info, syncConfig) => { const { address, initialAccount, currency, derivationMode } = info; const alpacaApi = (0, alpaca_1.getAlpacaApi)(currency.id, kind); const bridgeApi = (0, bridge_1.getBridgeApi)(currency, network); const chainSpecificValidation = bridgeApi.getChainSpecificRules?.(); if (chainSpecificValidation) { chainSpecificValidation.getAccountShape(address); } const accountId = (0, index_1.encodeAccountId)({ type: "js", version: "2", currencyId: currency.id, xpubOrAddress: address, derivationMode, }); const blockInfo = await alpacaApi.lastBlock(); const balanceRes = await alpacaApi.getBalance(address); const nativeAsset = (0, utils_1.extractBalance)(balanceRes, "native"); const allTokenAssetsBalances = balanceRes.filter(b => b.asset.type !== "native"); const nativeBalance = BigInt(nativeAsset?.value ?? "0"); const spendableBalance = BigInt(nativeBalance - BigInt(nativeAsset?.locked ?? "0")); // Normalize pre-alpaca operations to the new accountId to keep UI rendering consistent const oldOps = (initialAccount?.operations || []).map(op => op.accountId === accountId ? op : { ...op, accountId, id: (0, operation_1.encodeOperationId)(accountId, op.hash, op.type) }); const cursor = oldOps[0]?.extra?.pagingToken || ""; const syncHash = await (0, index_1.getSyncHash)(currency.id, syncConfig.blacklistedTokenIds); const syncFromScratch = !initialAccount?.blockHeight || initialAccount?.syncHash !== syncHash; // Calculate minHeight for pagination const minHeight = syncFromScratch ? 0 : (oldOps[0]?.blockHeight ?? 0) + 1; const paginationCursor = cursor && !syncFromScratch ? cursor : undefined; const { items: newCoreOps } = await alpacaApi.listOperations(address, { minHeight, cursor: paginationCursor, order: "desc", }); const newOps = newCoreOps .filter(op => !isNftCoreOp(op) && (!isIncomingCoreOp(op) || !op.tx.failed)) .map(op => (0, utils_1.adaptCoreOperationToLiveOperation)(accountId, op)); const newAssetOperations = newOps.filter(operation => operation?.extra?.assetReference && operation?.extra?.assetOwner && !["OPT_IN", "OPT_OUT"].includes(operation.type)); const newInternalOperations = []; const newNonInternalOperations = []; for (const op of newOps) { if (isInternalLiveOp(op)) newInternalOperations.push(op); else newNonInternalOperations.push(op); } const newSubAccounts = await (0, buildSubAccounts_1.buildSubAccounts)({ accountId, allTokenAssetsBalances, syncConfig, operations: newAssetOperations, getTokenFromAsset: bridgeApi.getTokenFromAsset, }); const subAccounts = syncFromScratch ? newSubAccounts : (0, buildSubAccounts_1.mergeSubAccounts)(initialAccount?.subAccounts ?? [], newSubAccounts); const newOpsWithSubs = buildParentOperations(newSubAccounts, newNonInternalOperations, newInternalOperations, accountId, address); // Try to refresh known pending and broadcasted operations (if not already updated) // Useful for integrations without explorers const operationsToRefresh = initialAccount?.pendingOperations.filter(pendingOp => pendingOp.hash && // operation has been broadcasted !newOpsWithSubs.some(newOp => pendingOp.hash === newOp.hash)); const confirmedOperations = alpacaApi.refreshOperations && operationsToRefresh?.length ? await alpacaApi.refreshOperations(operationsToRefresh) : []; const newOperations = [...confirmedOperations, ...newOpsWithSubs]; const operations = (0, jsHelpers_1.mergeOps)(syncFromScratch ? [] : oldOps, newOperations); const res = { id: accountId, xpub: address, blockHeight: operations.length === 0 ? 0 : blockInfo.height || initialAccount?.blockHeight, balance: new bignumber_js_1.default(nativeBalance.toString()), spendableBalance: new bignumber_js_1.default(spendableBalance.toString()), operations, subAccounts, operationsCount: operations.length, syncHash, }; return res; }; } //# sourceMappingURL=getAccountShape.js.map