// SPDX-FileCopyrightText: © 2026 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import type { StellarCoinConfig } from '../../config' import type { DeepPartialReturn } from '@ledgerhq/coin-module-framework/test/utils' import { Horizon, NotFoundError } from '@stellar/stellar-sdk' import { fetchAllLedgerOperations, fetchLedgerRecord } from '../horizon' const HORIZON = 'https://horizon-testnet.stellar.org' let currentFetchLimit = 100 const coinConfig = (): StellarCoinConfig => ({ status: { type: 'active' }, name: 'Stellar', unit: { name: 'Lumen', code: 'XLM', magnitude: 7 }, explorer: { url: `${HORIZON}/`, fetchLimit: currentFetchLimit }, }) /** Minimal Horizon payment op shape (embedded.records). */ const MINIMAL_HORIZON_PAYMENT_OP = { _links: { self: { href: '' }, transaction: { href: '' }, effects: { href: '' }, succeeds: { href: '' }, precedes: { href: '' }, }, id: '1', paging_token: '1', transaction_successful: true, source_account: 'GB7FW6GRFEH63Q3BVRT65VMB26OKHQRG74QRFUMTL6D553UKDZCDM7U', type: 'payment', type_i: 1, created_at: '2015-07-20T20:27:50Z', transaction_hash: 'txh', asset_type: 'native', from: 'GB7FW6GRFEH63Q3BVRT65VMB26OKHQRG74QRFUMTL6D553UKDZCDM7U', to: 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN', amount: '1.0000000', } function setExplorer(fetchLimit = 100): void { currentFetchLimit = fetchLimit } function mockLedgers(call: () => Promise): void { jest.spyOn(Horizon.Server.prototype, 'ledgers') jest .mocked>( Horizon.Server.prototype.ledgers ) .mockReturnValue({ ledger: jest.fn().mockReturnThis(), call }) } function mockOperations(call: () => Promise): void { jest.spyOn(Horizon.Server.prototype, 'operations') jest .mocked>( Horizon.Server.prototype.operations ) .mockReturnValue({ forLedger: jest.fn().mockReturnThis(), includeFailed: jest.fn().mockReturnThis(), join: jest.fn().mockReturnThis(), order: jest.fn().mockReturnThis(), limit: jest.fn().mockReturnValue({ call }), }) } describe('fetchLedgerRecord / fetchAllLedgerOperations', () => { beforeEach(() => { setExplorer(100) }) afterEach(() => { jest.restoreAllMocks() }) it('fetchLedgerRecord maps 404 to not found error', async () => { mockLedgers(() => Promise.reject(new NotFoundError('Not Found', {}))) await expect(fetchLedgerRecord(coinConfig(), 424242)).rejects.toThrow( 'Stellar ledger 424242 not found' ) }) it('fetchAllLedgerOperations maps 404 to not found error', async () => { mockOperations(() => Promise.reject(new NotFoundError('Not Found', {}))) await expect(fetchAllLedgerOperations(coinConfig(), 424243)).rejects.toThrow( 'Stellar ledger 424243 not found' ) }) it('fetchLedgerRecord returns ledger on 200', async () => { const ledgerBody = { _links: { self: { href: '' }, transactions: { href: '' }, operations: { href: '' } }, id: 'abc', paging_token: 'abc', hash: '0000000000000000000000000000000000000000000000000000000000000000', prev_hash: '', sequence: 3, successful_transaction_count: 0, failed_transaction_count: 0, operation_count: 0, tx_set_operation_count: 0, closed_at: '2015-07-20T20:27:50Z', total_coins: '100000000000.0000000', fee_pool: '0.0000000', base_fee_in_stroops: 100, base_reserve_in_stroops: 100000000, max_tx_set_size: 50, protocol_version: 1, header_xdr: '', } mockLedgers(() => Promise.resolve(ledgerBody)) const rec = await fetchLedgerRecord(coinConfig(), 3) expect(rec.sequence).toBe(3) expect(rec.hash).toBe(ledgerBody.hash) }) it('fetchAllLedgerOperations returns embedded records on 200', async () => { const page = { records: [MINIMAL_HORIZON_PAYMENT_OP], next: () => Promise.resolve({ records: [] }), } mockOperations(() => Promise.resolve(page)) const records = await fetchAllLedgerOperations(coinConfig(), 10) expect(records).toHaveLength(1) expect(records[0].type).toBe('payment') }) it('fetchAllLedgerOperations paginates when page is full then stops on empty page', async () => { setExplorer(1) const page2 = { records: [], next: () => Promise.resolve({ records: [] }) } const page1 = { records: [MINIMAL_HORIZON_PAYMENT_OP], next: () => Promise.resolve(page2) } mockOperations(() => Promise.resolve(page1)) const records = await fetchAllLedgerOperations(coinConfig(), 11) expect(records.length).toBe(1) }) })