@ledgerhq/coin-stellar
Version:
Ledger Stellar Coin integration
134 lines (120 loc) • 4.05 kB
text/typescript
// SPDX-FileCopyrightText: © 2026 LEDGER SAS
// SPDX-License-Identifier: Apache-2.0
import { BadResponseError } from '@stellar/stellar-sdk'
import {
documentationSummaryFromHorizonBody,
getHorizonErrorBody,
isHorizonErrorBody,
} from './horizonErrorBody'
describe('isHorizonErrorBody', () => {
it('accepts RFC 7807 problem bodies with title and status', () => {
expect(
isHorizonErrorBody({
type: 'https://stellar.org/horizon-errors/transaction_malformed',
title: 'Transaction Malformed',
status: 400,
detail: 'Horizon could not decode the transaction envelope in this request.',
})
).toBe(true)
})
it('rejects non-problem payloads', () => {
expect(isHorizonErrorBody({ title: 'Missing status' })).toBe(false)
expect(isHorizonErrorBody(null)).toBe(false)
})
})
describe('getHorizonErrorBody', () => {
const body = {
type: 'https://stellar.org/horizon-errors/bad_request',
title: 'Bad Request',
status: 400,
detail: 'Invalid cursor.',
extras: { reason: 'cursor is invalid' },
}
it('reads BadResponseError.response', () => {
const error = new BadResponseError('bad', body)
expect(getHorizonErrorBody(error)).toEqual(body)
})
it('reads axios-like error.response.data', () => {
const error = Object.assign(new Error('Request failed with status code 400'), {
response: { data: body },
})
expect(getHorizonErrorBody(error)).toEqual(body)
})
})
describe('documentationSummaryFromHorizonBody', () => {
const genericDetail =
'The transaction failed when submitted to the Stellar network. The `extras.result_codes` field on this response contains further details.'
it('prefers operation result code over generic tx_failed', () => {
expect(
documentationSummaryFromHorizonBody({
title: 'Transaction Failed',
status: 400,
detail: genericDetail,
extras: {
result_codes: {
transaction: 'tx_failed',
operations: ['op_underfunded'],
},
},
})
).toBe(
'The source account does not have enough balance to send the payment amount while maintaining its minimum reserve.'
)
})
it('skips op_inner and uses the first failing operation code', () => {
expect(
documentationSummaryFromHorizonBody({
title: 'Transaction Failed',
status: 400,
detail: genericDetail,
extras: {
result_codes: {
transaction: 'tx_failed',
operations: ['op_inner', 'op_underfunded'],
},
},
})
).toBe(
'The source account does not have enough balance to send the payment amount while maintaining its minimum reserve.'
)
})
it('uses transaction code when it is specific', () => {
expect(
documentationSummaryFromHorizonBody({
title: 'Transaction Failed',
status: 400,
detail: genericDetail,
extras: {
result_codes: { transaction: 'tx_bad_seq' },
},
})
).toBe('Sequence number does not match source account.')
})
it('uses Horizon detail when no result codes are present', () => {
expect(
documentationSummaryFromHorizonBody({
title: 'Transaction Malformed',
status: 400,
detail: 'Horizon could not decode the transaction envelope in this request.',
extras: { envelope_xdr: 'AAAA' },
})
).toBe('Horizon could not decode the transaction envelope in this request.')
})
it('uses extras.reason for validation errors', () => {
expect(
documentationSummaryFromHorizonBody({
title: 'Bad Request',
status: 400,
extras: { invalid_field: 'cursor', reason: 'cursor is invalid' },
})
).toBe('cursor is invalid (field: cursor)')
})
it('falls back to title when nothing else is available', () => {
expect(
documentationSummaryFromHorizonBody({
title: 'Rate Limited',
status: 429,
})
).toBe('Rate Limited')
})
})