UNPKG

@ledgerhq/coin-stellar

Version:
250 lines 8.46 kB
// SPDX-FileCopyrightText: © 2026 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import BigNumber from 'bignumber.js'; import { fetchAllLedgerOperations, fetchLedgerRecord } from '../network'; import { decodeMemo, parseSequence } from '../network/serialization'; import { parseAPIValue } from './common'; import { assertUnreachable } from './utils'; const NATIVE_ASSET = { type: 'native', name: 'XLM' }; /** * Horizon `type` strings that `getBlock` maps into `BlockOperation` entries. * * Same subset as user-facing history in `rawOperationsToOperations` (serialization); * other operation kinds are ignored until a later iteration expands coverage. */ const SUPPORTED_GETBLOCK_OP_TYPES = new Set([ 'create_account', 'payment', 'path_payment_strict_send', 'path_payment_strict_receive', 'change_trust', ]); function pathStrictDualLegTransfers(params) { const { fromAddr, toAddr, sourceAsset, destAsset, sourceStroops, destStroops } = params; const ops = []; if (fromAddr && sourceStroops !== 0n) { ops.push({ type: 'transfer', address: fromAddr, ...(toAddr && { peer: toAddr }), asset: sourceAsset, amount: -sourceStroops, }); } if (toAddr && destStroops !== 0n) { ops.push({ type: 'transfer', address: toAddr, ...(fromAddr && { peer: fromAddr }), asset: destAsset, amount: destStroops, }); } return ops; } function toStroops(amountStr) { if (!amountStr) { return 0n; } return BigInt(parseAPIValue(amountStr).integerValue(BigNumber.ROUND_FLOOR).toFixed(0)); } function assetFromHorizonFields(assetType, assetCode, assetIssuer) { if (!assetType || assetType === 'native') { return NATIVE_ASSET; } return { type: 'token', assetReference: assetCode ?? '', assetOwner: assetIssuer ?? '', }; } function isSupportedGetBlockOperation(op) { return SUPPORTED_GETBLOCK_OP_TYPES.has(op.type); } function orderTransactionHashes(rawOps) { const order = []; const seen = new Set(); for (const op of rawOps) { const h = op.transaction_hash; if (!seen.has(h)) { seen.add(h); order.push(h); } } return order; } function groupOpsByHash(rawOps) { const map = new Map(); for (const op of rawOps) { const h = op.transaction_hash; const list = map.get(h); if (list) list.push(op); else map.set(h, [op]); } return map; } function mapPaymentLikeTransfer(fromAddr, toAddr, amountStr, asset) { const amount = toStroops(amountStr); if (amount === 0n) { return []; } const ops = []; if (fromAddr) { ops.push({ type: 'transfer', address: fromAddr, ...(toAddr && { peer: toAddr }), asset, amount: -amount, }); } if (toAddr) { ops.push({ type: 'transfer', address: toAddr, ...(fromAddr && { peer: fromAddr }), asset, amount, }); } return ops; } function blockOperationsPathStrictSend(op) { const destAsset = assetFromHorizonFields(op.asset_type, op.asset_code, op.asset_issuer); const sourceAsset = assetFromHorizonFields(op.source_asset_type, op.source_asset_code, op.source_asset_issuer); return pathStrictDualLegTransfers({ fromAddr: op.from, toAddr: op.to, sourceAsset, destAsset, sourceStroops: toStroops(op.source_amount), destStroops: toStroops(op.amount), }); } function blockOperationsPathStrictReceive(op) { const destAsset = assetFromHorizonFields(op.asset_type, op.asset_code, op.asset_issuer); const sourceAsset = assetFromHorizonFields(op.source_asset_type, op.source_asset_code, op.source_asset_issuer); // Horizon reports actual source debit in `source_amount`; `send_max` is only the envelope cap. const sourceStroops = toStroops(op.source_amount ?? op.send_max); return pathStrictDualLegTransfers({ fromAddr: op.from, toAddr: op.to, sourceAsset, destAsset, sourceStroops, destStroops: toStroops(op.amount), }); } function mapSupportedOperationToBlockOperations(op, txCtx) { if (op.type === 'payment') { const asset = assetFromHorizonFields(op.asset_type, op.asset_code, op.asset_issuer); const toAddr = op.to_muxed || op.to; return mapPaymentLikeTransfer(op.from, toAddr, op.amount, asset); } if (op.type === 'create_account') { return mapPaymentLikeTransfer(op.funder, op.account, op.starting_balance, NATIVE_ASSET); } if (op.type === 'change_trust') { const isOptOut = new BigNumber(op.limit || '0').eq(0); // Align with listOperations: assetAmount = fee_charged (not the trustline limit) return [ { type: 'other', ledgerOpType: isOptOut ? 'OPT_OUT' : 'OPT_IN', assetAmount: op.asset_code ? txCtx.feeCharged : undefined, ...(txCtx.memo && { memo: txCtx.memo }), ...(txCtx.sequence && { sequence: txCtx.sequence }), }, ]; } if (op.type === 'path_payment_strict_send') { return blockOperationsPathStrictSend(op); } if (op.type === 'path_payment_strict_receive') { return blockOperationsPathStrictReceive(op); } return assertUnreachable(op); } async function blockTransactionForHash(hash, ops) { if (ops.length === 0) return null; const failed = !ops[0].transaction_successful; const tx = await ops[0].transaction(); const txCtx = { memo: decodeMemo(tx), sequence: parseSequence(tx.source_account_sequence), feeCharged: new BigNumber(tx.fee_charged || '0').toString(), }; let blockOperations = []; if (!failed) { for (const op of ops) { if (isSupportedGetBlockOperation(op)) { blockOperations = blockOperations.concat(mapSupportedOperationToBlockOperations(op, txCtx)); } } } const fees = BigInt(tx.fee_charged || '0'); const feesPayer = tx.fee_account || tx.source_account; return { hash, failed, fees, ...(feesPayer ? { feesPayer } : {}), operations: blockOperations, }; } async function buildBlockTransactions(rawOps) { const order = orderTransactionHashes(rawOps); const byHash = groupOpsByHash(rawOps); const out = []; for (const hash of order) { // `order` and `byHash` are built from the same `rawOps`; every ordered hash has a non-empty op list. const ops = byHash.get(hash); const row = await blockTransactionForHash(hash, ops); if (row) { out.push(row); } } return out; } /** * Returns the Stellar closed ledger at `height` (ledger sequence) with mapped * `BlockTransaction` entries (see `SUPPORTED_GETBLOCK_OP_TYPES` for which Horizon * operations are included). * * Only supported payment / path / trust / create-account operations contribute * balance operations; other Horizon operation types are skipped for successful * transactions. Failed transactions still appear with `failed: true`, fees * populated, and empty `operations`. */ export async function getBlock(height) { if (!Number.isSafeInteger(height) || height <= 0) { throw new Error(`getBlock: height must be a positive integer, got ${height}`); } const [ledger, rawOps] = await Promise.all([ fetchLedgerRecord(height), fetchAllLedgerOperations(height), ]); let parent; if (height > 1) { const prevHash = ledger.prev_hash; if (prevHash) { parent = { height: height - 1, hash: prevHash }; } else { const parentLedger = await fetchLedgerRecord(height - 1); parent = { height: parentLedger.sequence, hash: parentLedger.hash }; } } const info = { height: ledger.sequence, hash: ledger.hash, time: new Date(ledger.closed_at), ...(parent && { parent }), }; const transactions = await buildBlockTransactions(rawOps); return { info, transactions }; } //# sourceMappingURL=getBlock.js.map