@ledgerhq/coin-stellar 6.26.1 → 6.26.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ /** Horizon RFC 7807 problem+json body (subset used by coin-stellar). */
2
+ export type HorizonErrorBody = {
3
+ type?: string;
4
+ title?: string;
5
+ status?: number;
6
+ detail?: string;
7
+ extras?: {
8
+ envelope_xdr?: string;
9
+ result_xdr?: string;
10
+ result_codes?: {
11
+ transaction?: string;
12
+ operations?: string[];
13
+ };
14
+ invalid_field?: string;
15
+ reason?: string;
16
+ };
17
+ };
18
+ export declare function isHorizonErrorBody(data: unknown): data is HorizonErrorBody;
19
+ /**
20
+ * Horizon failures are usually {@link BadResponseError} with `response` = problem+json body.
21
+ * Axios rejects first; the SDK forwards it unchanged, so the body lives on `error.response.data`.
22
+ */
23
+ export declare function getHorizonErrorBody(error: unknown): HorizonErrorBody | null;
24
+ /**
25
+ * Best-effort human-readable summary for logs (`errorExtras.documentationSummary`).
26
+ * Prefers operation result codes, then transaction codes, then Horizon `detail` / `extras.reason`.
27
+ */
28
+ export declare function documentationSummaryFromHorizonBody(body: HorizonErrorBody): string;
29
+ //# sourceMappingURL=horizonErrorBody.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"horizonErrorBody.d.ts","sourceRoot":"","sources":["../../src/network/horizonErrorBody.ts"],"names":[],"mappings":"AAKA,wEAAwE;AACxE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE;QACP,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,YAAY,CAAC,EAAE;YACb,WAAW,CAAC,EAAE,MAAM,CAAA;YACpB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;SACtB,CAAA;QACD,aAAa,CAAC,EAAE,MAAM,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF,CAAA;AA2DD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,gBAAgB,CAS1E;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAW3E;AAiCD;;;GAGG;AACH,wBAAgB,mCAAmC,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAkClF"}
@@ -0,0 +1,134 @@
1
+ // SPDX-FileCopyrightText: © 2026 LEDGER SAS
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { BadResponseError } from '@stellar/stellar-sdk';
4
+ const GENERIC_TRANSACTION_FAILED_DETAIL = /The transaction failed when submitted to the Stellar network/i;
5
+ /** Horizon `extras.result_codes.transaction` → one-line description (Stellar docs). */
6
+ const HORIZON_TRANSACTION_RESULT_CODE_DOCUMENTATION = {
7
+ tx_success: 'The transaction succeeded.',
8
+ tx_failed: 'One of the operations failed (none were applied).',
9
+ tx_too_early: 'The ledger closeTime was before the minTime.',
10
+ tx_too_late: 'The ledger closeTime was after the maxTime.',
11
+ tx_missing_operation: 'No operation was specified.',
12
+ tx_bad_seq: 'Sequence number does not match source account.',
13
+ tx_bad_auth: 'Too few valid signatures / wrong network.',
14
+ tx_insufficient_balance: 'Fee would bring account below reserve.',
15
+ tx_no_source_account: 'Source account not found.',
16
+ tx_insufficient_fee: 'Fee is too small.',
17
+ tx_bad_auth_extra: 'Unused signatures attached to transaction.',
18
+ tx_internal_error: 'An unknown error occurred.',
19
+ tx_fee_bump_inner_success: 'Fee bump inner transaction succeeded.',
20
+ tx_fee_bump_inner_failed: 'Fee bump inner transaction failed.',
21
+ tx_not_supported: 'Transaction type not supported.',
22
+ tx_bad_sponsorship: 'Sponsorship is invalid.',
23
+ tx_bad_min_seq_age_or_gap: 'Minimum sequence age or gap precondition failed.',
24
+ tx_malformed: 'Transaction is malformed.',
25
+ tx_soroban_invalid: 'Soroban transaction is invalid.',
26
+ };
27
+ /** Horizon `extras.result_codes.operations[]` → one-line description (Stellar docs). */
28
+ const HORIZON_OPERATION_RESULT_CODE_DOCUMENTATION = {
29
+ op_inner: 'The inner object result is valid and the operation was a success.',
30
+ op_bad_auth: 'There are too few valid signatures, or the transaction was submitted to the wrong network.',
31
+ op_no_source_account: 'The source account was not found.',
32
+ op_no_account: 'The source account was not found.',
33
+ op_not_supported: 'The operation is not supported at this time.',
34
+ op_too_many_subentries: 'Max number of subentries (1000) already reached.',
35
+ op_exceeded_work_limit: 'Operation did too much work.',
36
+ op_malformed: 'The operation input is invalid.',
37
+ op_underfunded: 'The source account does not have enough balance to send the payment amount while maintaining its minimum reserve.',
38
+ op_src_no_trust: 'The source account does not have a trustline for the asset it is trying to send.',
39
+ op_src_not_authorized: 'The source account is not authorized to send this asset.',
40
+ op_no_destination: 'The destination account does not exist.',
41
+ op_no_trust: 'The destination account does not have a trustline for the asset being sent.',
42
+ op_not_authorized: 'The destination account is not authorized to hold this asset.',
43
+ op_line_full: 'The destination account does not have sufficient limits to receive the amount and still satisfy its buying liabilities.',
44
+ op_no_issuer: 'The issuer of the asset does not exist.',
45
+ };
46
+ /** Operation result codes that mark success for a prior op in a multi-op failure list. */
47
+ const HORIZON_OPERATION_SUCCESS_RESULT_CODES = new Set(['op_inner']);
48
+ function failingOperationCode(operations) {
49
+ return operations?.find((code) => !HORIZON_OPERATION_SUCCESS_RESULT_CODES.has(code));
50
+ }
51
+ export function isHorizonErrorBody(data) {
52
+ return !!(data &&
53
+ typeof data === 'object' &&
54
+ 'title' in data &&
55
+ typeof data.title === 'string' &&
56
+ 'status' in data &&
57
+ typeof data.status === 'number');
58
+ }
59
+ /**
60
+ * Horizon failures are usually {@link BadResponseError} with `response` = problem+json body.
61
+ * Axios rejects first; the SDK forwards it unchanged, so the body lives on `error.response.data`.
62
+ */
63
+ export function getHorizonErrorBody(error) {
64
+ if (error instanceof BadResponseError && isHorizonErrorBody(error.response)) {
65
+ return error.response;
66
+ }
67
+ if (error && typeof error === 'object' && 'response' in error) {
68
+ const data = error.response?.data;
69
+ if (isHorizonErrorBody(data)) {
70
+ return data;
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+ function lookupOperationDocumentation(operationCode) {
76
+ if (!operationCode) {
77
+ return undefined;
78
+ }
79
+ return HORIZON_OPERATION_RESULT_CODE_DOCUMENTATION[operationCode];
80
+ }
81
+ function lookupTransactionDocumentation(transactionCode) {
82
+ if (!transactionCode) {
83
+ return undefined;
84
+ }
85
+ return HORIZON_TRANSACTION_RESULT_CODE_DOCUMENTATION[transactionCode];
86
+ }
87
+ function detailDocumentation(body) {
88
+ const detail = body.detail?.trim();
89
+ if (!detail || GENERIC_TRANSACTION_FAILED_DETAIL.test(detail)) {
90
+ return undefined;
91
+ }
92
+ return detail;
93
+ }
94
+ function extrasReasonDocumentation(body) {
95
+ const reason = body.extras?.reason?.trim();
96
+ if (!reason) {
97
+ return undefined;
98
+ }
99
+ const invalidField = body.extras?.invalid_field?.trim();
100
+ return invalidField ? `${reason} (field: ${invalidField})` : reason;
101
+ }
102
+ /**
103
+ * Best-effort human-readable summary for logs (`errorExtras.documentationSummary`).
104
+ * Prefers operation result codes, then transaction codes, then Horizon `detail` / `extras.reason`.
105
+ */
106
+ export function documentationSummaryFromHorizonBody(body) {
107
+ const transactionCode = body.extras?.result_codes?.transaction;
108
+ const operationCode = failingOperationCode(body.extras?.result_codes?.operations);
109
+ const operationSummary = lookupOperationDocumentation(operationCode);
110
+ if (operationSummary) {
111
+ return operationSummary;
112
+ }
113
+ const transactionSummary = lookupTransactionDocumentation(transactionCode);
114
+ if (transactionSummary && transactionCode !== 'tx_failed') {
115
+ return transactionSummary;
116
+ }
117
+ const detail = detailDocumentation(body);
118
+ if (detail) {
119
+ return detail;
120
+ }
121
+ const extrasReason = extrasReasonDocumentation(body);
122
+ if (extrasReason) {
123
+ return extrasReason;
124
+ }
125
+ if (transactionSummary) {
126
+ return transactionSummary;
127
+ }
128
+ const title = body.title?.trim();
129
+ if (title) {
130
+ return title;
131
+ }
132
+ return 'Unknown Horizon error.';
133
+ }
134
+ //# sourceMappingURL=horizonErrorBody.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"horizonErrorBody.js","sourceRoot":"","sources":["../../src/network/horizonErrorBody.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"horizonLedgerErrors.d.ts","sourceRoot":"","sources":["../../src/network/horizonLedgerErrors.ts"],"names":[],"mappings":"AAMA,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAQ5D;AASD,wBAAgB,mCAAmC,CAAC,CAAC,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,GAAG,KAAK,CAwB9F"}
1
+ {"version":3,"file":"horizonLedgerErrors.d.ts","sourceRoot":"","sources":["../../src/network/horizonLedgerErrors.ts"],"names":[],"mappings":"AAOA,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAY5D;AASD,wBAAgB,mCAAmC,CAAC,CAAC,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,GAAG,KAAK,CA2C9F"}
@@ -2,7 +2,12 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import { LedgerAPI4xx, LedgerAPI5xx, NetworkDown } from '@ledgerhq/errors';
4
4
  import { NetworkError, NotFoundError } from '@stellar/stellar-sdk';
5
+ import { documentationSummaryFromHorizonBody, getHorizonErrorBody } from './horizonErrorBody';
5
6
  export function messageFromHorizonUnknown(e) {
7
+ const body = getHorizonErrorBody(e);
8
+ if (body) {
9
+ return documentationSummaryFromHorizonBody(body);
10
+ }
6
11
  if (e instanceof Error) {
7
12
  return e.message;
8
13
  }
@@ -18,8 +23,25 @@ const HORIZON_STATUS_5XX = /status code 5\d{2}/;
18
23
  const HORIZON_NETWORK_DOWN = /ECONNRESET|ECONNREFUSED|ENOTFOUND|EPIPE|ETIMEDOUT/;
19
24
  const HORIZON_UNDEFINED_OBJECT = /undefined is not an object/;
20
25
  export function throwHorizonLedgerOrOperationsError(e, notFoundMessage) {
26
+ if (e instanceof NotFoundError) {
27
+ throw new Error(notFoundMessage);
28
+ }
29
+ const body = getHorizonErrorBody(e);
30
+ const status = body?.status;
31
+ if (status === 404) {
32
+ throw new Error(notFoundMessage);
33
+ }
34
+ if (status === 429) {
35
+ throw new LedgerAPI4xx('status code 4xx', { status: 429, url: undefined, method: 'GET' });
36
+ }
37
+ if (status !== undefined && status >= 400 && status < 500) {
38
+ throw new LedgerAPI4xx();
39
+ }
40
+ if (status !== undefined && status >= 500 && status < 600) {
41
+ throw new LedgerAPI5xx();
42
+ }
21
43
  const errorMsg = messageFromHorizonUnknown(e);
22
- if (e instanceof NotFoundError || HORIZON_STATUS_404.exec(errorMsg)) {
44
+ if (HORIZON_STATUS_404.exec(errorMsg)) {
23
45
  throw new Error(notFoundMessage);
24
46
  }
25
47
  if (HORIZON_TOO_MANY_REQUESTS.exec(errorMsg)) {
@@ -1 +1 @@
1
- {"version":3,"file":"horizonLedgerErrors.js","sourceRoot":"","sources":["../../src/network/horizonLedgerErrors.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,sCAAsC;AAEtC,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC1E,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAElE,MAAM,UAAU,yBAAyB,CAAC,CAAU;IAClD,IAAI,CAAC,YAAY,KAAK,EAAE,CAAC;QACvB,OAAO,CAAC,CAAC,OAAO,CAAA;IAClB,CAAC;IACD,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC1B,OAAO,CAAC,CAAA;IACV,CAAC;IACD,OAAO,EAAE,CAAA;AACX,CAAC;AAED,MAAM,kBAAkB,GAAG,iBAAiB,CAAA;AAC5C,MAAM,yBAAyB,GAAG,oBAAoB,CAAA;AACtD,MAAM,kBAAkB,GAAG,oBAAoB,CAAA;AAC/C,MAAM,kBAAkB,GAAG,oBAAoB,CAAA;AAC/C,MAAM,oBAAoB,GAAG,mDAAmD,CAAA;AAChF,MAAM,wBAAwB,GAAG,4BAA4B,CAAA;AAE7D,MAAM,UAAU,mCAAmC,CAAC,CAAU,EAAE,eAAuB;IACrF,MAAM,QAAQ,GAAG,yBAAyB,CAAC,CAAC,CAAC,CAAA;IAE7C,IAAI,CAAC,YAAY,aAAa,IAAI,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpE,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAA;IAClC,CAAC;IACD,IAAI,yBAAyB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,YAAY,CAAC,iBAAiB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAA;IAC3F,CAAC;IACD,IAAI,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,YAAY,EAAE,CAAA;IAC1B,CAAC;IACD,IAAI,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,YAAY,EAAE,CAAA;IAC1B,CAAC;IACD,IACE,CAAC,YAAY,YAAY;QACzB,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC;QACnC,wBAAwB,CAAC,IAAI,CAAC,QAAQ,CAAC,EACvC,CAAC;QACD,MAAM,IAAI,WAAW,EAAE,CAAA;IACzB,CAAC;IAED,MAAM,CAAC,CAAA;AACT,CAAC"}
1
+ {"version":3,"file":"horizonLedgerErrors.js","sourceRoot":"","sources":["../../src/network/horizonLedgerErrors.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ledgerhq/coin-stellar",
3
- "version": "6.26.1",
3
+ "version": "6.26.2",
4
4
  "description": "Ledger Stellar Coin integration",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -82,7 +82,7 @@
82
82
  "@stellar/stellar-sdk": "^14.0.0",
83
83
  "bignumber.js": "^9.1.2",
84
84
  "@exodus/patch-broken-hermes-typed-arrays": "1.0.0-alpha.1",
85
- "@ledgerhq/coin-module-framework": "^3.5.1"
85
+ "@ledgerhq/coin-module-framework": "^3.6.0"
86
86
  },
87
87
  "peerDependencies": {
88
88
  "@ledgerhq/errors": "^6.33.0",
@@ -233,7 +233,7 @@ describe('Stellar Api', () => {
233
233
 
234
234
  /**
235
235
  * Signed transaction envelope from BACK-10960 / Jira (mainnet).
236
- * Live Horizon may report `tx_failed` + XDR `txFailed` (operation-level failure) while static ticket JSON showed `tx_no_source_account`.
236
+ * Live Horizon reports `tx_failed` with operation-level `op_underfunded`.
237
237
  */
238
238
  describe('broadcast', () => {
239
239
  let broadcastModule: CoinModuleApi<StellarMemo>
@@ -250,8 +250,10 @@ describe('Stellar Api', () => {
250
250
  it('maps Horizon transaction failure to StellarBroadcastFailedError with XDR context', async () => {
251
251
  await expect(broadcastModule.broadcast(tx)).rejects.toMatchObject({
252
252
  name: 'StellarBroadcastFailedError',
253
- documentationSummary: 'One of the operations failed (none were applied).',
253
+ documentationSummary:
254
+ 'The source account does not have enough balance to send the payment amount while maintaining its minimum reserve.',
254
255
  horizonTransactionCode: 'tx_failed',
256
+ horizonOperationCodes: ['op_underfunded'],
255
257
  stellarDocUrl:
256
258
  'https://developers.stellar.org/docs/data/apis/horizon/api-reference/errors/result-codes/transactions',
257
259
  decodedResultXdr: {
@@ -75,6 +75,26 @@ describe('broadcastTransaction', () => {
75
75
  })
76
76
  })
77
77
 
78
+ it('maps malformed Horizon body without result_codes to StellarBroadcastFailedError', async () => {
79
+ const body = {
80
+ type: 'https://stellar.org/horizon-errors/transaction_malformed',
81
+ title: 'Transaction Malformed',
82
+ status: 400,
83
+ detail: 'Horizon could not decode the transaction envelope in this request.',
84
+ extras: { envelope_xdr: tx },
85
+ }
86
+ submitTransactionSpy.mockRejectedValue(
87
+ new BadResponseError('Transaction submission failed. Server responded: 400 Bad Request', body)
88
+ )
89
+
90
+ await expect(broadcastTransaction(tx)).rejects.toMatchObject({
91
+ name: 'StellarBroadcastFailedError',
92
+ documentationSummary: 'Horizon could not decode the transaction envelope in this request.',
93
+ horizonTransactionCode: '',
94
+ cause: expect.any(BadResponseError),
95
+ })
96
+ })
97
+
78
98
  it('rethrows a plain error with no Horizon payload unchanged (same reference)', async () => {
79
99
  const original = new Error('ECONNREFUSED')
80
100
  submitTransactionSpy.mockRejectedValue(original)
@@ -7,7 +7,6 @@ import { LedgerAPI4xx, LedgerAPI5xx, NetworkDown } from '@ledgerhq/errors'
7
7
  import { makeLRUCache } from '@ledgerhq/live-network/cache'
8
8
  import { log } from '@ledgerhq/logs'
9
9
  import {
10
- BadResponseError,
11
10
  BASE_FEE,
12
11
  Horizon,
13
12
  MuxedAccount,
@@ -30,6 +29,11 @@ import {
30
29
  NetworkCongestionLevel,
31
30
  } from '../types'
32
31
  import { StellarBroadcastFailedError, type StellarDecodedResultXdr } from '../types/errors'
32
+ import {
33
+ documentationSummaryFromHorizonBody,
34
+ getHorizonErrorBody,
35
+ type HorizonErrorBody,
36
+ } from './horizonErrorBody'
33
37
  import { throwHorizonLedgerOrOperationsError } from './horizonLedgerErrors'
34
38
  import { getReservedBalance, rawOperationsToOperations } from './serialization'
35
39
 
@@ -41,75 +45,6 @@ const FETCH_LIMIT = 100
41
45
  const STELLAR_TX_RESULT_CODES_DOC_URL =
42
46
  'https://developers.stellar.org/docs/data/apis/horizon/api-reference/errors/result-codes/transactions'
43
47
 
44
- /** Horizon `extras.result_codes.transaction` → one-line description (Stellar docs). */
45
- const HORIZON_TRANSACTION_RESULT_CODE_DOCUMENTATION: Record<string, string> = {
46
- tx_success: 'The transaction succeeded.',
47
- tx_failed: 'One of the operations failed (none were applied).',
48
- tx_too_early: 'The ledger closeTime was before the minTime.',
49
- tx_too_late: 'The ledger closeTime was after the maxTime.',
50
- tx_missing_operation: 'No operation was specified.',
51
- tx_bad_seq: 'Sequence number does not match source account.',
52
- tx_bad_auth: 'Too few valid signatures / wrong network.',
53
- tx_insufficient_balance: 'Fee would bring account below reserve.',
54
- tx_no_source_account: 'Source account not found.',
55
- tx_insufficient_fee: 'Fee is too small.',
56
- tx_bad_auth_extra: 'Unused signatures attached to transaction.',
57
- tx_internal_error: 'An unknown error occurred.',
58
- tx_fee_bump_inner_success: 'Fee bump inner transaction succeeded.',
59
- tx_fee_bump_inner_failed: 'Fee bump inner transaction failed.',
60
- tx_not_supported: 'Transaction type not supported.',
61
- tx_bad_sponsorship: 'Sponsorship is invalid.',
62
- tx_bad_min_seq_age_or_gap: 'Minimum sequence age or gap precondition failed.',
63
- tx_malformed: 'Transaction is malformed.',
64
- tx_soroban_invalid: 'Soroban transaction is invalid.',
65
- }
66
-
67
- type HorizonSubmitErrorBody = {
68
- type?: string
69
- title?: string
70
- status?: number
71
- detail?: string
72
- extras?: {
73
- envelope_xdr?: string
74
- result_xdr?: string
75
- result_codes?: {
76
- transaction?: string
77
- operations?: string[]
78
- }
79
- }
80
- }
81
-
82
- function isHorizonSubmitErrorBody(data: unknown): data is HorizonSubmitErrorBody {
83
- return !!(
84
- data &&
85
- typeof data === 'object' &&
86
- 'extras' in data &&
87
- typeof data.extras === 'object' &&
88
- data.extras !== null &&
89
- 'result_codes' in data.extras &&
90
- data.extras.result_codes !== undefined &&
91
- typeof data.extras.result_codes === 'object'
92
- )
93
- }
94
-
95
- /**
96
- * Horizon submit failures are usually {@link BadResponseError} with `response` = problem+json body.
97
- * In practice axios rejects with `AxiosError` first; the SDK's `.catch` forwards it unchanged (see js-stellar-sdk horizon/server.js),
98
- * so the Horizon body lives on `error.response.data`.
99
- */
100
- function getHorizonBodyFromSubmitFailure(error: unknown): HorizonSubmitErrorBody | null {
101
- if (error instanceof BadResponseError && isHorizonSubmitErrorBody(error.response)) {
102
- return error.response
103
- }
104
- if (error && typeof error === 'object' && 'response' in error) {
105
- const data = (error as { response?: { data?: unknown } }).response?.data
106
- if (isHorizonSubmitErrorBody(data)) {
107
- return data
108
- }
109
- }
110
- return null
111
- }
112
-
113
48
  function decodeTransactionResultFields(resultXdrBase64: string | undefined):
114
49
  | {
115
50
  feeChargedStroops?: string
@@ -136,14 +71,11 @@ function decodeTransactionResultFields(resultXdrBase64: string | undefined):
136
71
  }
137
72
  }
138
73
 
139
- function makeBroadcastFailedError(body: HorizonSubmitErrorBody, cause: Error): Error {
74
+ function makeBroadcastFailedError(body: HorizonErrorBody, cause: Error): Error {
140
75
  const extras = body.extras
141
76
  const horizonTransactionCode = extras?.result_codes?.transaction ?? ''
142
77
  const horizonOperationCodes = extras?.result_codes?.operations
143
- const documentationSummary =
144
- (horizonTransactionCode &&
145
- HORIZON_TRANSACTION_RESULT_CODE_DOCUMENTATION[horizonTransactionCode]) ||
146
- 'Unknown transaction result code.'
78
+ const documentationSummary = documentationSummaryFromHorizonBody(body)
147
79
  const decodedResult = decodeTransactionResultFields(extras?.result_xdr)
148
80
  const feeChargedStroops = decodedResult?.feeChargedStroops
149
81
  const resultXdrSwitchName = decodedResult?.resultXdrSwitchName
@@ -558,7 +490,7 @@ export async function fetchSigners(account: string): Promise<Signer[]> {
558
490
  }
559
491
 
560
492
  function interpretedError(error: unknown): unknown {
561
- const body = getHorizonBodyFromSubmitFailure(error)
493
+ const body = getHorizonErrorBody(error)
562
494
  if (body) {
563
495
  const cause = error instanceof Error ? error : new Error(String(error))
564
496
  return makeBroadcastFailedError(body, cause)
@@ -382,7 +382,7 @@ describe('horizon.ts (unit, spies)', () => {
382
382
  .mockRejectedValue(new Error('mock not configured'))
383
383
  })
384
384
 
385
- it('uses Unknown transaction result code when Horizon code is not in the map', async () => {
385
+ it('uses Horizon detail as documentationSummary when transaction result code is unknown', async () => {
386
386
  const body = {
387
387
  type: 'https://stellar.org/horizon-errors/transaction_failed',
388
388
  title: 'Transaction Failed',
@@ -403,7 +403,7 @@ describe('horizon.ts (unit, spies)', () => {
403
403
 
404
404
  await expect(broadcastTransaction(signedTxFixture)).rejects.toMatchObject({
405
405
  name: 'StellarBroadcastFailedError',
406
- documentationSummary: 'Unknown transaction result code.',
406
+ documentationSummary: 'failed',
407
407
  })
408
408
  })
409
409
 
@@ -0,0 +1,133 @@
1
+ // SPDX-FileCopyrightText: © 2026 LEDGER SAS
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { BadResponseError } from '@stellar/stellar-sdk'
5
+ import {
6
+ documentationSummaryFromHorizonBody,
7
+ getHorizonErrorBody,
8
+ isHorizonErrorBody,
9
+ } from './horizonErrorBody'
10
+
11
+ describe('isHorizonErrorBody', () => {
12
+ it('accepts RFC 7807 problem bodies with title and status', () => {
13
+ expect(
14
+ isHorizonErrorBody({
15
+ type: 'https://stellar.org/horizon-errors/transaction_malformed',
16
+ title: 'Transaction Malformed',
17
+ status: 400,
18
+ detail: 'Horizon could not decode the transaction envelope in this request.',
19
+ })
20
+ ).toBe(true)
21
+ })
22
+
23
+ it('rejects non-problem payloads', () => {
24
+ expect(isHorizonErrorBody({ title: 'Missing status' })).toBe(false)
25
+ expect(isHorizonErrorBody(null)).toBe(false)
26
+ })
27
+ })
28
+
29
+ describe('getHorizonErrorBody', () => {
30
+ const body = {
31
+ type: 'https://stellar.org/horizon-errors/bad_request',
32
+ title: 'Bad Request',
33
+ status: 400,
34
+ detail: 'Invalid cursor.',
35
+ extras: { reason: 'cursor is invalid' },
36
+ }
37
+
38
+ it('reads BadResponseError.response', () => {
39
+ const error = new BadResponseError('bad', body)
40
+ expect(getHorizonErrorBody(error)).toEqual(body)
41
+ })
42
+
43
+ it('reads axios-like error.response.data', () => {
44
+ const error = Object.assign(new Error('Request failed with status code 400'), {
45
+ response: { data: body },
46
+ })
47
+ expect(getHorizonErrorBody(error)).toEqual(body)
48
+ })
49
+ })
50
+
51
+ describe('documentationSummaryFromHorizonBody', () => {
52
+ const genericDetail =
53
+ 'The transaction failed when submitted to the Stellar network. The `extras.result_codes` field on this response contains further details.'
54
+
55
+ it('prefers operation result code over generic tx_failed', () => {
56
+ expect(
57
+ documentationSummaryFromHorizonBody({
58
+ title: 'Transaction Failed',
59
+ status: 400,
60
+ detail: genericDetail,
61
+ extras: {
62
+ result_codes: {
63
+ transaction: 'tx_failed',
64
+ operations: ['op_underfunded'],
65
+ },
66
+ },
67
+ })
68
+ ).toBe(
69
+ 'The source account does not have enough balance to send the payment amount while maintaining its minimum reserve.'
70
+ )
71
+ })
72
+
73
+ it('skips op_inner and uses the first failing operation code', () => {
74
+ expect(
75
+ documentationSummaryFromHorizonBody({
76
+ title: 'Transaction Failed',
77
+ status: 400,
78
+ detail: genericDetail,
79
+ extras: {
80
+ result_codes: {
81
+ transaction: 'tx_failed',
82
+ operations: ['op_inner', 'op_underfunded'],
83
+ },
84
+ },
85
+ })
86
+ ).toBe(
87
+ 'The source account does not have enough balance to send the payment amount while maintaining its minimum reserve.'
88
+ )
89
+ })
90
+
91
+ it('uses transaction code when it is specific', () => {
92
+ expect(
93
+ documentationSummaryFromHorizonBody({
94
+ title: 'Transaction Failed',
95
+ status: 400,
96
+ detail: genericDetail,
97
+ extras: {
98
+ result_codes: { transaction: 'tx_bad_seq' },
99
+ },
100
+ })
101
+ ).toBe('Sequence number does not match source account.')
102
+ })
103
+
104
+ it('uses Horizon detail when no result codes are present', () => {
105
+ expect(
106
+ documentationSummaryFromHorizonBody({
107
+ title: 'Transaction Malformed',
108
+ status: 400,
109
+ detail: 'Horizon could not decode the transaction envelope in this request.',
110
+ extras: { envelope_xdr: 'AAAA' },
111
+ })
112
+ ).toBe('Horizon could not decode the transaction envelope in this request.')
113
+ })
114
+
115
+ it('uses extras.reason for validation errors', () => {
116
+ expect(
117
+ documentationSummaryFromHorizonBody({
118
+ title: 'Bad Request',
119
+ status: 400,
120
+ extras: { invalid_field: 'cursor', reason: 'cursor is invalid' },
121
+ })
122
+ ).toBe('cursor is invalid (field: cursor)')
123
+ })
124
+
125
+ it('falls back to title when nothing else is available', () => {
126
+ expect(
127
+ documentationSummaryFromHorizonBody({
128
+ title: 'Rate Limited',
129
+ status: 429,
130
+ })
131
+ ).toBe('Rate Limited')
132
+ })
133
+ })