UNPKG

@ledgerhq/coin-stellar

Version:
119 lines (101 loc) 4.39 kB
// SPDX-FileCopyrightText: © 2026 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import { BadResponseError, Horizon } from '@stellar/stellar-sdk' import coinConfig from '../config' import { broadcastTransaction } from './horizon' /** Parses on mainnet; used only so `submitTransaction` is reached (mocked). */ const tx = 'AAAAAgAAAABRUCgFba+DTbei2ifpyYt5w2Hh0VyZ+X9fayjIDne7YAAAAGQCkDOGAAAABQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAACEIuPfWXgM8WhyqjrpFdIcGV1SUVhMzPUm4YspNHF60QAAAAAAAAAAALkd2QAAAAAAAAABDne7YAAAAEAASzsT/yDIfCfEDstkfnznXjiN7rNd7PkKQEn+rRIFm9EHoirGfHipWoBdYMrc6ixQD/0y0of1piSid8TLiFAB' function horizonTransactionFailedExtras() { return { type: 'https://stellar.org/horizon-errors/transaction_failed', title: 'Transaction Failed', status: 400, detail: 'The transaction failed when submitted to the stellar network.', extras: { result_codes: { transaction: 'tx_failed' as const, operations: ['op_no_account'] as string[], }, result_xdr: 'AAAAAAAAAGT////4AAAAAA==', envelope_xdr: tx, }, } } describe('broadcastTransaction', () => { let submitTransactionSpy: jest.SpiedFunction<Horizon.Server['submitTransaction']> beforeAll(() => { coinConfig.setCoinConfig(() => ({ explorer: { url: 'https://horizon-broadcast-unit-test.invalid/' }, status: { type: 'active' }, })) }) beforeEach(() => { submitTransactionSpy = jest .spyOn(Horizon.Server.prototype, 'submitTransaction') .mockRejectedValue(new Error('mock not configured')) }) afterEach(() => { submitTransactionSpy.mockRestore() }) it('maps BadResponseError with Horizon body on error.response to StellarBroadcastFailedError', async () => { const body = horizonTransactionFailedExtras() submitTransactionSpy.mockRejectedValue( new BadResponseError('Transaction submission failed. Server responded: 400 Bad Request', body) ) await expect(broadcastTransaction(tx)).rejects.toMatchObject({ name: 'StellarBroadcastFailedError', horizonTransactionCode: 'tx_failed', cause: expect.any(BadResponseError), }) }) it('maps axios-like error with Horizon body on error.response.data to StellarBroadcastFailedError', async () => { const body = horizonTransactionFailedExtras() submitTransactionSpy.mockRejectedValue( Object.assign(new Error('Request failed with status code 400'), { name: 'AxiosError', response: { status: 400, statusText: 'Bad Request', data: body }, }) ) await expect(broadcastTransaction(tx)).rejects.toMatchObject({ name: 'StellarBroadcastFailedError', horizonTransactionCode: 'tx_failed', cause: expect.objectContaining({ name: 'AxiosError' }), }) }) it('maps malformed Horizon body without result_codes to StellarBroadcastFailedError', async () => { const body = { type: 'https://stellar.org/horizon-errors/transaction_malformed', title: 'Transaction Malformed', status: 400, detail: 'Horizon could not decode the transaction envelope in this request.', extras: { envelope_xdr: tx }, } submitTransactionSpy.mockRejectedValue( new BadResponseError('Transaction submission failed. Server responded: 400 Bad Request', body) ) await expect(broadcastTransaction(tx)).rejects.toMatchObject({ name: 'StellarBroadcastFailedError', documentationSummary: 'Horizon could not decode the transaction envelope in this request.', horizonTransactionCode: '', cause: expect.any(BadResponseError), }) }) it('rethrows a plain error with no Horizon payload unchanged (same reference)', async () => { const original = new Error('ECONNREFUSED') submitTransactionSpy.mockRejectedValue(original) await expect(broadcastTransaction(tx)).rejects.toBe(original) }) it('rethrows an axios-like error when response.data is not a Horizon transaction failure body', async () => { const original = Object.assign(new Error('Request failed with status code 500'), { name: 'AxiosError', response: { status: 500, statusText: 'Internal Server Error', data: { title: 'Internal Server Error' }, }, }) submitTransactionSpy.mockRejectedValue(original) await expect(broadcastTransaction(tx)).rejects.toBe(original) }) })