@ledgerhq/coin-tezos
Version:
471 lines • 18.4 kB
JavaScript
"use strict";
// SPDX-FileCopyrightText: © 2026 LEDGER SAS
// SPDX-License-Identifier: Apache-2.0
Object.defineProperty(exports, "__esModule", { value: true });
exports.getBlock = getBlock;
const constants_1 = require("../constants");
const network_1 = require("../network");
const NATIVE_ASSET = { type: 'native', name: 'XTZ' };
// ---------------------------------------------------------------------------
// Block-info mapping
// ---------------------------------------------------------------------------
function mapBlockInfo(block, parentBlock) {
return {
height: block.level,
hash: block.hash,
time: new Date(block.timestamp),
parent: { height: parentBlock.level, hash: parentBlock.hash },
};
}
// ---------------------------------------------------------------------------
// Native XTZ helpers
// ---------------------------------------------------------------------------
/** Computes bakerFee + storageFee + allocationFee for a single operation. */
function computeOpFees(op) {
return BigInt(op.bakerFee ?? 0) + BigInt(op.storageFee ?? 0) + BigInt(op.allocationFee ?? 0);
}
/**
* Computes the total fees for an operation group (all ops sharing the same hash).
*
* In Tezos, `bakerFee` is charged only on the top-level op, but `storageFee` and
* `allocationFee` can appear on internal ops emitted by contracts as well.
*/
function computeFees(group) {
return group.reduce((sum, op) => sum + computeOpFees(op), 0n);
}
/**
* Determines who paid the fees for an operation group.
*
* The fee payer is the top-level op's `initiator` (when a contract triggered the
* call) or its `sender` (for direct user operations). The "top-level" op is the
* one that carries a non-zero `bakerFee`; if none exists we fall back to the first
* op in the group.
*/
function findFeesPayer(group) {
const topLevelOp = group.find((op) => (op.bakerFee ?? 0) > 0) ?? group[0];
return topLevelOp?.initiator?.address ?? topLevelOp?.sender?.address;
}
/**
* Returns `true` only when every op in the group succeeded.
*
* Tezos can partially execute a batch (later ops get "backtracked"), so we treat
* the whole group as failed if any op did not reach "applied" status. Fees are
* still charged in that case, but no balance changes took effect.
*/
function isGroupSucceeded(group) {
return group.every((op) => !op.status || op.status === 'applied');
}
/**
* Produces one outgoing and one incoming `BlockOperation` for each transaction in the group
* that should be represented in the block.
*
* Transactions are skipped only when both the transferred amount and the fees are zero.
* Fee-only transactions (amount === 0 but fees > 0) are kept so fee attribution and
* sender/target linkage match `listOperations`.
*
* Fees are intentionally excluded from amounts — they are reported separately in
* `BlockTransaction.fees`. In Tezos, the `amount` field on `APITransactionType`
* already represents only the transferred value, so no adjustment is required
* (unlike XRPL, where the raw balance diff includes the fee deduction).
*/
function buildNativeOperations(group) {
const ops = [];
for (const tx of group) {
const amount = BigInt(tx.amount ?? 0);
if (amount === 0n && computeOpFees(tx) === 0n)
continue;
const fromAddr = tx.sender?.address;
const toAddr = tx.target?.address;
if (fromAddr) {
ops.push({
type: 'transfer',
address: fromAddr,
...(toAddr && { peer: toAddr }),
asset: NATIVE_ASSET,
amount: -amount,
});
}
if (toAddr) {
ops.push({
type: 'transfer',
address: toAddr,
...(fromAddr && { peer: fromAddr }),
asset: NATIVE_ASSET,
amount,
});
}
}
return ops;
}
// ---------------------------------------------------------------------------
// Delegation helpers
// ---------------------------------------------------------------------------
const computeDelegationFees = computeOpFees;
function buildDelegationOperations(op) {
if (!op.sender?.address)
return [];
const isDelegate = !!op.newDelegate?.address;
const opType = isDelegate ? 'DELEGATE' : 'UNDELEGATE';
return [
{
type: 'other',
ledgerOpType: opType,
operationType: opType,
stakedAmount: 0,
counter: op.counter,
gasLimit: op.gasLimit,
storageLimit: op.storageLimit,
},
];
}
function buildBlockTransactionFromDelegation(op) {
if (!op.hash)
return null;
const feesPayer = op.sender?.address;
const succeeded = !op.status || op.status === 'applied';
return {
hash: op.hash,
failed: !succeeded,
fees: computeDelegationFees(op),
...(feesPayer && { feesPayer }),
// Include operations even for failed standalone txs — listOperations path
// always converts them. Note: mergeAuxiliaryTx still clears ops when a
// failed auxiliary tx is merged into an existing BlockTransaction.
operations: buildDelegationOperations(op),
};
}
// ---------------------------------------------------------------------------
// Staking helpers (Paris adaptive issuance)
// ---------------------------------------------------------------------------
const computeStakingFees = computeOpFees;
function buildStakingOperations(op) {
if (!op.sender?.address)
return [];
const operationType = constants_1.STAKING_ACTION_TO_OP_TYPE[op.action];
return [
{
type: 'other',
ledgerOpType: operationType,
operationType,
stakedAmount: Number(op.amount ?? op.requestedAmount ?? 0),
counter: op.counter,
gasLimit: op.gasLimit,
storageLimit: op.storageLimit,
},
];
}
function buildBlockTransactionFromStaking(op) {
if (!op.hash)
return null;
const feesPayer = op.sender?.address;
const succeeded = !op.status || op.status === 'applied';
return {
hash: op.hash,
failed: !succeeded,
fees: computeStakingFees(op),
...(feesPayer && { feesPayer }),
operations: buildStakingOperations(op),
};
}
// ---------------------------------------------------------------------------
// Origination helpers
// ---------------------------------------------------------------------------
const computeOriginationFees = computeOpFees;
function buildOriginationOperations(op) {
const senderAddr = op.sender?.address;
if (!senderAddr)
return [];
return [
{
type: 'other',
address: senderAddr,
ledgerOpType: 'ORIGINATION',
counter: op.counter,
gasLimit: op.gasLimit,
storageLimit: op.storageLimit,
},
];
}
function buildBlockTransactionFromOrigination(op) {
if (!op.hash)
return null;
const feesPayer = op.sender?.address;
const succeeded = !op.status || op.status === 'applied';
return {
hash: op.hash,
failed: !succeeded,
fees: computeOriginationFees(op),
...(feesPayer && { feesPayer }),
operations: buildOriginationOperations(op),
};
}
// ---------------------------------------------------------------------------
// Reveal helpers
// ---------------------------------------------------------------------------
const computeRevealFees = computeOpFees;
function buildRevealOperations(op) {
const senderAddr = op.sender?.address;
if (!senderAddr)
return [];
return [
{
type: 'other',
address: senderAddr,
ledgerOpType: 'REVEAL',
counter: op.counter,
gasLimit: op.gasLimit,
storageLimit: op.storageLimit,
},
];
}
function buildBlockTransactionFromReveal(op) {
if (!op.hash)
return null;
const feesPayer = op.sender?.address;
const succeeded = !op.status || op.status === 'applied';
return {
hash: op.hash,
failed: !succeeded,
fees: computeRevealFees(op),
...(feesPayer && { feesPayer }),
operations: buildRevealOperations(op),
};
}
// ---------------------------------------------------------------------------
// FA token helpers
// ---------------------------------------------------------------------------
/**
* Produces outgoing / incoming `BlockOperation` entries for a single FA transfer.
*
* Minting events (`from` absent) produce only the incoming entry; burning events
* (`to` absent) produce only the outgoing entry.
*/
function buildTokenOperations(transfer) {
const tokenAmount = BigInt(transfer.amount);
if (tokenAmount === 0n)
return [];
// For FA2, multiple token IDs coexist under one contract address.
// Encoding both as "address:tokenId" makes every token uniquely identifiable.
// FA1.2 tokens always have tokenId "0", so this format is safe for both standards.
const tokenId = transfer.token.tokenId ?? '0';
const asset = {
type: transfer.token.standard,
assetReference: `${transfer.token.contract.address}:${tokenId}`,
name: transfer.token.metadata?.name ?? transfer.token.metadata?.symbol,
};
const fromAddr = transfer.from?.address;
const toAddr = transfer.to?.address;
const ops = [];
if (fromAddr) {
ops.push({
type: 'transfer',
address: fromAddr,
...(toAddr && { peer: toAddr }),
asset,
amount: -tokenAmount,
});
}
if (toAddr) {
ops.push({
type: 'transfer',
address: toAddr,
...(fromAddr && { peer: fromAddr }),
asset,
amount: tokenAmount,
});
}
return ops;
}
// ---------------------------------------------------------------------------
// Transaction grouping — private helpers
// ---------------------------------------------------------------------------
/** Groups native XTZ transactions by their operation hash. */
function groupTransactionsByHash(transactions) {
const groups = new Map();
for (const tx of transactions) {
if (!tx.hash)
continue;
const existing = groups.get(tx.hash);
if (existing)
existing.push(tx);
else
groups.set(tx.hash, [tx]);
}
return groups;
}
/** Builds a `BlockTransaction` from a group of ops that share the same hash. */
function buildBlockTransactionFromGroup(hash, group) {
const succeeded = isGroupSucceeded(group);
const blockTx = {
hash,
failed: !succeeded,
fees: computeFees(group),
operations: succeeded ? buildNativeOperations(group) : [],
};
const feesPayer = findFeesPayer(group);
if (feesPayer)
blockTx.feesPayer = feesPayer;
return blockTx;
}
/**
* Attaches a single FA token transfer to the appropriate `BlockTransaction`.
*
* When the transfer's parent native tx is found (via `transactionId → hash`),
* the token ops are appended to it (unless it failed). Otherwise a standalone
* entry is created or extended in `standaloneByKey`, grouped by `transactionId`
* so that sibling transfers from the same on-chain operation share one entry.
*/
function attachTokenTransfer(transfer, txIdToHash, blockTxByHash, standaloneByKey) {
const tokenOps = buildTokenOperations(transfer);
if (tokenOps.length === 0)
return;
const parentHash = transfer.transactionId !== undefined
? txIdToHash.get(transfer.transactionId)
: transfer.originationId !== undefined
? txIdToHash.get(transfer.originationId)
: undefined;
if (parentHash !== undefined && blockTxByHash.has(parentHash)) {
const parent = blockTxByHash.get(parentHash);
if (!parent.failed)
parent.operations.push(...tokenOps);
return;
}
// No matching BlockTransaction in this block. Use the resolved parent hash
// when available (cross-block origination) so the standalone entry carries the
// real origination hash — matching what listOperations would produce.
const parentId = transfer.transactionId ?? transfer.originationId;
const key = parentHash ?? (parentId === undefined ? `token-${transfer.id}` : `txid-${parentId}`);
const existing = standaloneByKey.get(key);
if (existing) {
existing.operations.push(...tokenOps);
}
else {
standaloneByKey.set(key, {
hash: key,
failed: false,
fees: 0n,
operations: tokenOps,
});
}
}
// ---------------------------------------------------------------------------
// Transaction grouping — public orchestrator
// ---------------------------------------------------------------------------
function mergeAuxiliaryTx(blockTxByHash, auxTx) {
const existing = blockTxByHash.get(auxTx.hash);
if (!existing) {
blockTxByHash.set(auxTx.hash, auxTx);
return;
}
// When an auxiliary op (reveal, delegation, etc.) failed, we mark the whole
// merged transaction as failed. This is a modeling choice: the auxiliary op
// shares the same hash and its failure typically means the batch was aborted.
if (auxTx.failed) {
existing.failed = true;
existing.operations = [];
}
else if (!existing.failed && auxTx.operations.length > 0) {
existing.operations.push(...auxTx.operations);
}
existing.fees += auxTx.fees;
}
function groupAndMapTransactions(transactions, tokenTransfers, delegations, stakings, originations, reveals, crossBlockIdToHash = new Map()) {
const groups = groupTransactionsByHash(transactions);
const txIdToHash = new Map();
for (const tx of transactions) {
if (tx.id && tx.hash)
txIdToHash.set(tx.id, tx.hash);
}
for (const orig of originations) {
if (orig.id && orig.hash)
txIdToHash.set(orig.id, orig.hash);
}
for (const [id, hash] of crossBlockIdToHash) {
txIdToHash.set(id, hash);
}
const blockTxByHash = new Map();
for (const [hash, group] of groups) {
blockTxByHash.set(hash, buildBlockTransactionFromGroup(hash, group));
}
for (const delegation of delegations) {
const delegationTx = buildBlockTransactionFromDelegation(delegation);
if (delegationTx)
mergeAuxiliaryTx(blockTxByHash, delegationTx);
}
for (const staking of stakings) {
const stakingTx = buildBlockTransactionFromStaking(staking);
if (stakingTx)
mergeAuxiliaryTx(blockTxByHash, stakingTx);
}
for (const origination of originations) {
const originationTx = buildBlockTransactionFromOrigination(origination);
if (originationTx)
mergeAuxiliaryTx(blockTxByHash, originationTx);
}
for (const reveal of reveals) {
const revealTx = buildBlockTransactionFromReveal(reveal);
if (revealTx)
mergeAuxiliaryTx(blockTxByHash, revealTx);
}
const standaloneByKey = new Map();
for (const transfer of tokenTransfers) {
attachTokenTransfer(transfer, txIdToHash, blockTxByHash, standaloneByKey);
}
return [...blockTxByHash.values(), ...standaloneByKey.values()];
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Returns the full block at the given Tezos level: metadata + all transactions
* with their XTZ and FA token balance changes.
*
* - Fetches block metadata, native transactions, delegations, FA token transfers, and staking operations in parallel.
* - Also fetches the predecessor block in parallel to populate `BlockInfo.parent`.
* - Groups operations by hash, aggregates fees, and determines the fee payer.
* - Appends FA token transfer operations to the owning BlockTransaction when a
* matching `transactionId` can be resolved; otherwise creates a standalone entry.
*/
async function getBlock(context, height) {
if (!Number.isSafeInteger(height) || height <= 0) {
throw new Error(`getBlock: height must be a positive integer, got ${height}`);
}
const config = await context.config();
const tzkt = (0, network_1.createTzktApi)(config);
const [block, parentBlock, transactions, tokenTransfers, delegations, stakings, originations, reveals,] = await Promise.all([
tzkt.getBlockByLevel(height),
tzkt.getBlockByLevel(height - 1),
(0, network_1.fetchBlockTransactions)(config, height),
(0, network_1.fetchBlockTokenTransfers)(config, height),
(0, network_1.fetchBlockDelegations)(config, height),
(0, network_1.fetchBlockStaking)(config, height),
(0, network_1.fetchBlockOriginations)(config, height),
(0, network_1.fetchBlockReveals)(config, height),
]);
// Token transfers triggered by originations from other blocks carry an
// `originationId` that points outside this block's origination set. Resolve
// those hashes so the token ops attach to the correct parent BlockTransaction
// instead of becoming orphan `token-{id}` entries.
const knownIds = new Set([...transactions.map((t) => t.id), ...originations.map((o) => o.id)]);
const unresolvedOrigIds = [
...new Set(tokenTransfers
.filter((t) => t.transactionId === undefined)
.map((t) => t.originationId)
.filter((id) => id !== undefined && !knownIds.has(id))),
];
const crossBlockIdToHash = new Map();
if (unresolvedOrigIds.length > 0) {
const resolved = await tzkt.getOperationsOrigination(0, undefined, {
'id.in': unresolvedOrigIds.join(','),
});
for (const op of resolved) {
if (op.id && op.hash)
crossBlockIdToHash.set(op.id, op.hash);
}
}
return {
info: mapBlockInfo(block, parentBlock),
transactions: groupAndMapTransactions(transactions, tokenTransfers, delegations, stakings, originations, reveals, crossBlockIdToHash),
};
}
//# sourceMappingURL=getBlock.js.map