// SPDX-FileCopyrightText: © 2026 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import type { StellarCoinConfig, StellarContext } from '../../config' import type { DeepPartialReturn } from '@ledgerhq/coin-module-framework/test/utils' import * as network from '../../network' import { getBlockInfo } from '../getBlockInfo' jest.mock('../../network', () => ({ ...jest.requireActual('../../network'), fetchLedgerRecord: jest.fn(), })) const coinConfig: StellarCoinConfig = { explorer: { url: 'https://horizon.example.com' }, status: { type: 'active' }, name: 'Stellar', unit: { name: 'Lumen', code: 'XLM', magnitude: 7 }, } const context: StellarContext = { config: async () => coinConfig, logger: jest.fn(), } const fetchLedgerRecordMock = jest.mocked>( network.fetchLedgerRecord ) describe('getBlockInfo', () => { beforeEach(() => { jest.clearAllMocks() }) it('returns block info with parent when height > 1 (uses ledger prev_hash)', async () => { fetchLedgerRecordMock.mockImplementation(async (_config, seq) => ({ sequence: seq, hash: `hash-${seq}`, closed_at: `2020-01-0${seq}T00:00:00Z`, ...(seq > 1 && { prev_hash: `hash-${seq - 1}` }), })) const result = await getBlockInfo(context, 5) expect(fetchLedgerRecordMock).toHaveBeenCalledTimes(1) expect(fetchLedgerRecordMock).toHaveBeenCalledWith(coinConfig, 5) expect(result).toEqual({ height: 5, hash: 'hash-5', time: new Date('2020-01-05T00:00:00Z'), parent: { height: 4, hash: 'hash-4' }, }) }) it('falls back to fetching parent ledger when prev_hash is missing', async () => { fetchLedgerRecordMock.mockImplementation(async (_config, seq) => ({ sequence: seq, hash: `hash-${seq}`, closed_at: `2020-01-0${seq}T00:00:00Z`, })) const result = await getBlockInfo(context, 5) expect(fetchLedgerRecordMock).toHaveBeenCalledWith(coinConfig, 5) expect(fetchLedgerRecordMock).toHaveBeenCalledWith(coinConfig, 4) expect(fetchLedgerRecordMock).toHaveBeenCalledTimes(2) expect(result).toEqual({ height: 5, hash: 'hash-5', time: new Date('2020-01-05T00:00:00Z'), parent: { height: 4, hash: 'hash-4' }, }) }) it('omits parent for genesis ledger sequence 1', async () => { fetchLedgerRecordMock.mockResolvedValue({ sequence: 1, hash: 'genesis-hash', closed_at: '2015-09-30T00:00:00Z', }) const result = await getBlockInfo(context, 1) expect(fetchLedgerRecordMock).toHaveBeenCalledTimes(1) expect(fetchLedgerRecordMock).toHaveBeenCalledWith(coinConfig, 1) expect(result.parent).toBeUndefined() expect(result.height).toBe(1) }) describe('height validation', () => { it('rejects height 0', async () => { await expect(getBlockInfo(context, 0)).rejects.toThrow( 'getBlockInfo: height must be a positive integer, got 0' ) }) it('rejects negative height', async () => { await expect(getBlockInfo(context, -3)).rejects.toThrow( 'getBlockInfo: height must be a positive integer, got -3' ) }) it('rejects non-integer height', async () => { await expect(getBlockInfo(context, 3.5)).rejects.toThrow( 'getBlockInfo: height must be a positive integer, got 3.5' ) }) it('rejects NaN', async () => { await expect(getBlockInfo(context, Number.NaN)).rejects.toThrow( 'getBlockInfo: height must be a positive integer, got NaN' ) }) it('rejects unsafe integer', async () => { await expect(getBlockInfo(context, Number.MAX_SAFE_INTEGER + 1)).rejects.toThrow( 'getBlockInfo: height must be a positive integer' ) }) it('rejects Infinity', async () => { await expect(getBlockInfo(context, Number.POSITIVE_INFINITY)).rejects.toThrow( 'getBlockInfo: height must be a positive integer, got Infinity' ) }) }) })