@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
290 lines • 14.6 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.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