@ledgerhq/coin-stellar 6.26.0 → 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.
Files changed (281) hide show
  1. package/.oxlintrc.json +7 -0
  2. package/CHANGELOG.md +12 -0
  3. package/LICENSE +194 -0
  4. package/knip.json +12 -0
  5. package/lib/api/index.d.ts +5 -5
  6. package/lib/api/index.d.ts.map +1 -1
  7. package/lib/api/index.js +12 -10
  8. package/lib/api/index.js.map +1 -1
  9. package/lib/config.d.ts +1 -1
  10. package/lib/config.d.ts.map +1 -1
  11. package/lib/config.js +2 -0
  12. package/lib/config.js.map +1 -1
  13. package/lib/constants.d.ts.map +1 -1
  14. package/lib/constants.js +3 -1
  15. package/lib/constants.js.map +1 -1
  16. package/lib/logic/broadcast.d.ts.map +1 -1
  17. package/lib/logic/broadcast.js +2 -0
  18. package/lib/logic/broadcast.js.map +1 -1
  19. package/lib/logic/combine.d.ts +1 -1
  20. package/lib/logic/combine.d.ts.map +1 -1
  21. package/lib/logic/combine.js +2 -0
  22. package/lib/logic/combine.js.map +1 -1
  23. package/lib/logic/common.d.ts +1 -1
  24. package/lib/logic/common.d.ts.map +1 -1
  25. package/lib/logic/common.js +4 -2
  26. package/lib/logic/common.js.map +1 -1
  27. package/lib/logic/craftTransaction.d.ts +1 -1
  28. package/lib/logic/craftTransaction.d.ts.map +1 -1
  29. package/lib/logic/craftTransaction.js +10 -8
  30. package/lib/logic/craftTransaction.js.map +1 -1
  31. package/lib/logic/estimateFees.d.ts.map +1 -1
  32. package/lib/logic/estimateFees.js +2 -0
  33. package/lib/logic/estimateFees.js.map +1 -1
  34. package/lib/logic/getBalance.d.ts +1 -1
  35. package/lib/logic/getBalance.d.ts.map +1 -1
  36. package/lib/logic/getBalance.js +4 -2
  37. package/lib/logic/getBalance.js.map +1 -1
  38. package/lib/logic/getBlock.d.ts +1 -1
  39. package/lib/logic/getBlock.d.ts.map +1 -1
  40. package/lib/logic/getBlock.js +39 -34
  41. package/lib/logic/getBlock.js.map +1 -1
  42. package/lib/logic/getBlockInfo.d.ts +1 -1
  43. package/lib/logic/getBlockInfo.d.ts.map +1 -1
  44. package/lib/logic/getBlockInfo.js +2 -0
  45. package/lib/logic/getBlockInfo.js.map +1 -1
  46. package/lib/logic/index.d.ts +12 -12
  47. package/lib/logic/index.d.ts.map +1 -1
  48. package/lib/logic/index.js +2 -0
  49. package/lib/logic/index.js.map +1 -1
  50. package/lib/logic/lastBlock.d.ts +1 -1
  51. package/lib/logic/lastBlock.d.ts.map +1 -1
  52. package/lib/logic/lastBlock.js +2 -0
  53. package/lib/logic/lastBlock.js.map +1 -1
  54. package/lib/logic/listOperations.d.ts +1 -1
  55. package/lib/logic/listOperations.d.ts.map +1 -1
  56. package/lib/logic/listOperations.js +6 -4
  57. package/lib/logic/listOperations.js.map +1 -1
  58. package/lib/logic/sdkWrapper.d.ts +1 -1
  59. package/lib/logic/sdkWrapper.d.ts.map +1 -1
  60. package/lib/logic/sdkWrapper.js +2 -0
  61. package/lib/logic/sdkWrapper.js.map +1 -1
  62. package/lib/logic/utils.d.ts.map +1 -1
  63. package/lib/logic/utils.js +4 -2
  64. package/lib/logic/utils.js.map +1 -1
  65. package/lib/logic/validateAddress.d.ts +1 -1
  66. package/lib/logic/validateAddress.d.ts.map +1 -1
  67. package/lib/logic/validateAddress.js +2 -0
  68. package/lib/logic/validateAddress.js.map +1 -1
  69. package/lib/logic/validateIntent.d.ts +2 -2
  70. package/lib/logic/validateIntent.d.ts.map +1 -1
  71. package/lib/logic/validateIntent.js +21 -19
  72. package/lib/logic/validateIntent.js.map +1 -1
  73. package/lib/logic/validateMemo.d.ts +1 -1
  74. package/lib/logic/validateMemo.d.ts.map +1 -1
  75. package/lib/logic/validateMemo.js +7 -5
  76. package/lib/logic/validateMemo.js.map +1 -1
  77. package/lib/network/horizon.d.ts +6 -7
  78. package/lib/network/horizon.d.ts.map +1 -1
  79. package/lib/network/horizon.js +48 -95
  80. package/lib/network/horizon.js.map +1 -1
  81. package/lib/network/horizonErrorBody.d.ts +29 -0
  82. package/lib/network/horizonErrorBody.d.ts.map +1 -0
  83. package/lib/network/horizonErrorBody.js +139 -0
  84. package/lib/network/horizonErrorBody.js.map +1 -0
  85. package/lib/network/horizonLedgerErrors.d.ts.map +1 -1
  86. package/lib/network/horizonLedgerErrors.js +28 -4
  87. package/lib/network/horizonLedgerErrors.js.map +1 -1
  88. package/lib/network/index.d.ts +1 -1
  89. package/lib/network/index.d.ts.map +1 -1
  90. package/lib/network/index.js +2 -0
  91. package/lib/network/index.js.map +1 -1
  92. package/lib/network/serialization.d.ts +6 -4
  93. package/lib/network/serialization.d.ts.map +1 -1
  94. package/lib/network/serialization.js +95 -91
  95. package/lib/network/serialization.js.map +1 -1
  96. package/lib/polyfill.d.ts.map +1 -1
  97. package/lib/polyfill.js +6 -4
  98. package/lib/polyfill.js.map +1 -1
  99. package/lib/supportedFeatures.d.ts +1 -1
  100. package/lib/supportedFeatures.d.ts.map +1 -1
  101. package/lib/supportedFeatures.js +3 -1
  102. package/lib/supportedFeatures.js.map +1 -1
  103. package/lib/types/errors.d.ts.map +1 -1
  104. package/lib/types/errors.js +14 -12
  105. package/lib/types/errors.js.map +1 -1
  106. package/lib/types/index.d.ts +2 -2
  107. package/lib/types/index.d.ts.map +1 -1
  108. package/lib/types/index.js +2 -0
  109. package/lib/types/index.js.map +1 -1
  110. package/lib/types/model.d.ts +6 -6
  111. package/lib/types/model.d.ts.map +1 -1
  112. package/lib/types/model.js +7 -5
  113. package/lib/types/model.js.map +1 -1
  114. package/lib-es/api/index.d.ts +5 -5
  115. package/lib-es/api/index.d.ts.map +1 -1
  116. package/lib-es/api/index.js +20 -18
  117. package/lib-es/api/index.js.map +1 -1
  118. package/lib-es/config.d.ts +1 -1
  119. package/lib-es/config.d.ts.map +1 -1
  120. package/lib-es/config.js +3 -1
  121. package/lib-es/config.js.map +1 -1
  122. package/lib-es/constants.d.ts.map +1 -1
  123. package/lib-es/constants.js +3 -1
  124. package/lib-es/constants.js.map +1 -1
  125. package/lib-es/logic/broadcast.d.ts.map +1 -1
  126. package/lib-es/logic/broadcast.js +3 -1
  127. package/lib-es/logic/broadcast.js.map +1 -1
  128. package/lib-es/logic/combine.d.ts +1 -1
  129. package/lib-es/logic/combine.d.ts.map +1 -1
  130. package/lib-es/logic/combine.js +3 -1
  131. package/lib-es/logic/combine.js.map +1 -1
  132. package/lib-es/logic/common.d.ts +1 -1
  133. package/lib-es/logic/common.d.ts.map +1 -1
  134. package/lib-es/logic/common.js +5 -3
  135. package/lib-es/logic/common.js.map +1 -1
  136. package/lib-es/logic/craftTransaction.d.ts +1 -1
  137. package/lib-es/logic/craftTransaction.d.ts.map +1 -1
  138. package/lib-es/logic/craftTransaction.js +15 -13
  139. package/lib-es/logic/craftTransaction.js.map +1 -1
  140. package/lib-es/logic/estimateFees.d.ts.map +1 -1
  141. package/lib-es/logic/estimateFees.js +3 -1
  142. package/lib-es/logic/estimateFees.js.map +1 -1
  143. package/lib-es/logic/getBalance.d.ts +1 -1
  144. package/lib-es/logic/getBalance.d.ts.map +1 -1
  145. package/lib-es/logic/getBalance.js +5 -3
  146. package/lib-es/logic/getBalance.js.map +1 -1
  147. package/lib-es/logic/getBlock.d.ts +1 -1
  148. package/lib-es/logic/getBlock.d.ts.map +1 -1
  149. package/lib-es/logic/getBlock.js +42 -37
  150. package/lib-es/logic/getBlock.js.map +1 -1
  151. package/lib-es/logic/getBlockInfo.d.ts +1 -1
  152. package/lib-es/logic/getBlockInfo.d.ts.map +1 -1
  153. package/lib-es/logic/getBlockInfo.js +3 -1
  154. package/lib-es/logic/getBlockInfo.js.map +1 -1
  155. package/lib-es/logic/index.d.ts +12 -12
  156. package/lib-es/logic/index.d.ts.map +1 -1
  157. package/lib-es/logic/index.js +14 -12
  158. package/lib-es/logic/index.js.map +1 -1
  159. package/lib-es/logic/lastBlock.d.ts +1 -1
  160. package/lib-es/logic/lastBlock.d.ts.map +1 -1
  161. package/lib-es/logic/lastBlock.js +3 -1
  162. package/lib-es/logic/lastBlock.js.map +1 -1
  163. package/lib-es/logic/listOperations.d.ts +1 -1
  164. package/lib-es/logic/listOperations.d.ts.map +1 -1
  165. package/lib-es/logic/listOperations.js +7 -5
  166. package/lib-es/logic/listOperations.js.map +1 -1
  167. package/lib-es/logic/sdkWrapper.d.ts +1 -1
  168. package/lib-es/logic/sdkWrapper.d.ts.map +1 -1
  169. package/lib-es/logic/sdkWrapper.js +4 -2
  170. package/lib-es/logic/sdkWrapper.js.map +1 -1
  171. package/lib-es/logic/utils.d.ts.map +1 -1
  172. package/lib-es/logic/utils.js +6 -4
  173. package/lib-es/logic/utils.js.map +1 -1
  174. package/lib-es/logic/validateAddress.d.ts +1 -1
  175. package/lib-es/logic/validateAddress.d.ts.map +1 -1
  176. package/lib-es/logic/validateAddress.js +3 -1
  177. package/lib-es/logic/validateAddress.js.map +1 -1
  178. package/lib-es/logic/validateIntent.d.ts +2 -2
  179. package/lib-es/logic/validateIntent.d.ts.map +1 -1
  180. package/lib-es/logic/validateIntent.js +30 -28
  181. package/lib-es/logic/validateIntent.js.map +1 -1
  182. package/lib-es/logic/validateMemo.d.ts +1 -1
  183. package/lib-es/logic/validateMemo.d.ts.map +1 -1
  184. package/lib-es/logic/validateMemo.js +8 -6
  185. package/lib-es/logic/validateMemo.js.map +1 -1
  186. package/lib-es/network/horizon.d.ts +6 -7
  187. package/lib-es/network/horizon.d.ts.map +1 -1
  188. package/lib-es/network/horizon.js +57 -104
  189. package/lib-es/network/horizon.js.map +1 -1
  190. package/lib-es/network/horizonErrorBody.d.ts +29 -0
  191. package/lib-es/network/horizonErrorBody.d.ts.map +1 -0
  192. package/lib-es/network/horizonErrorBody.js +134 -0
  193. package/lib-es/network/horizonErrorBody.js.map +1 -0
  194. package/lib-es/network/horizonLedgerErrors.d.ts.map +1 -1
  195. package/lib-es/network/horizonLedgerErrors.js +30 -6
  196. package/lib-es/network/horizonLedgerErrors.js.map +1 -1
  197. package/lib-es/network/index.d.ts +1 -1
  198. package/lib-es/network/index.d.ts.map +1 -1
  199. package/lib-es/network/index.js +3 -1
  200. package/lib-es/network/index.js.map +1 -1
  201. package/lib-es/network/serialization.d.ts +6 -4
  202. package/lib-es/network/serialization.d.ts.map +1 -1
  203. package/lib-es/network/serialization.js +97 -95
  204. package/lib-es/network/serialization.js.map +1 -1
  205. package/lib-es/polyfill.d.ts.map +1 -1
  206. package/lib-es/polyfill.js +7 -5
  207. package/lib-es/polyfill.js.map +1 -1
  208. package/lib-es/supportedFeatures.d.ts +1 -1
  209. package/lib-es/supportedFeatures.d.ts.map +1 -1
  210. package/lib-es/supportedFeatures.js +3 -1
  211. package/lib-es/supportedFeatures.js.map +1 -1
  212. package/lib-es/types/errors.d.ts.map +1 -1
  213. package/lib-es/types/errors.js +15 -13
  214. package/lib-es/types/errors.js.map +1 -1
  215. package/lib-es/types/index.d.ts +2 -2
  216. package/lib-es/types/index.d.ts.map +1 -1
  217. package/lib-es/types/index.js +4 -2
  218. package/lib-es/types/index.js.map +1 -1
  219. package/lib-es/types/model.d.ts +6 -6
  220. package/lib-es/types/model.d.ts.map +1 -1
  221. package/lib-es/types/model.js +7 -5
  222. package/lib-es/types/model.js.map +1 -1
  223. package/package.json +13 -16
  224. package/src/api/index.integ.test.ts +168 -163
  225. package/src/api/index.test.ts +106 -103
  226. package/src/api/index.ts +49 -46
  227. package/src/api/index_error.test.ts +48 -25
  228. package/src/config.ts +15 -12
  229. package/src/constants.ts +4 -1
  230. package/src/globals.d.ts +7 -5
  231. package/src/logic/broadcast.integ.test.ts +22 -19
  232. package/src/logic/broadcast.ts +5 -2
  233. package/src/logic/combine.test.ts +18 -15
  234. package/src/logic/combine.ts +8 -5
  235. package/src/logic/common.ts +9 -6
  236. package/src/logic/craftTransaction.test.ts +76 -75
  237. package/src/logic/craftTransaction.ts +46 -43
  238. package/src/logic/estimateFees.ts +6 -3
  239. package/src/logic/getBalance.test.ts +31 -28
  240. package/src/logic/getBalance.ts +13 -10
  241. package/src/logic/getBlock.integ.test.ts +40 -39
  242. package/src/logic/getBlock.test.ts +462 -415
  243. package/src/logic/getBlock.ts +142 -127
  244. package/src/logic/getBlockInfo.test.ts +72 -69
  245. package/src/logic/getBlockInfo.ts +15 -12
  246. package/src/logic/index.ts +15 -12
  247. package/src/logic/lastBlock.ts +6 -3
  248. package/src/logic/listOperations.ts +14 -11
  249. package/src/logic/listOperations.unit.test.ts +212 -209
  250. package/src/logic/networkWrappers.test.ts +32 -29
  251. package/src/logic/sdkWrapper.ts +19 -16
  252. package/src/logic/utils.ts +13 -10
  253. package/src/logic/validateAddress.test.ts +21 -18
  254. package/src/logic/validateAddress.ts +7 -4
  255. package/src/logic/validateIntent.test.ts +62 -59
  256. package/src/logic/validateIntent.ts +87 -84
  257. package/src/logic/validateMemo.test.ts +50 -47
  258. package/src/logic/validateMemo.ts +16 -13
  259. package/src/network/broadcastTransaction.test.ts +83 -63
  260. package/src/network/fetchLedgerOperations.unit.test.ts +155 -0
  261. package/src/network/horizon.integ.test.ts +45 -42
  262. package/src/network/horizon.test.ts +17 -14
  263. package/src/network/horizon.ts +244 -314
  264. package/src/network/horizon.unit.test.ts +238 -237
  265. package/src/network/horizonErrorBody.test.ts +133 -0
  266. package/src/network/horizonErrorBody.ts +178 -0
  267. package/src/network/horizonLedgerErrors.test.ts +150 -78
  268. package/src/network/horizonLedgerErrors.ts +47 -20
  269. package/src/network/index.ts +4 -1
  270. package/src/network/registerHorizonInterceptors.test.ts +163 -160
  271. package/src/network/serialization.ts +232 -227
  272. package/src/network/serialization.unit.test.ts +801 -798
  273. package/src/polyfill.ts +18 -15
  274. package/src/supportedFeatures.ts +6 -3
  275. package/src/types/errors.ts +33 -32
  276. package/src/types/index.ts +5 -2
  277. package/src/types/model.ts +55 -52
  278. package/tsconfig.json +1 -1
  279. package/.unimportedrc.json +0 -17
  280. package/LICENSE.txt +0 -21
  281. package/src/network/fetchLedgerOperations.nock.unit.test.ts +0 -156
@@ -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
+ })
@@ -0,0 +1,178 @@
1
+ // SPDX-FileCopyrightText: © 2026 LEDGER SAS
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { BadResponseError } from '@stellar/stellar-sdk'
5
+
6
+ /** Horizon RFC 7807 problem+json body (subset used by coin-stellar). */
7
+ export type HorizonErrorBody = {
8
+ type?: string
9
+ title?: string
10
+ status?: number
11
+ detail?: string
12
+ extras?: {
13
+ envelope_xdr?: string
14
+ result_xdr?: string
15
+ result_codes?: {
16
+ transaction?: string
17
+ operations?: string[]
18
+ }
19
+ invalid_field?: string
20
+ reason?: string
21
+ }
22
+ }
23
+
24
+ const GENERIC_TRANSACTION_FAILED_DETAIL =
25
+ /The transaction failed when submitted to the Stellar network/i
26
+
27
+ /** Horizon `extras.result_codes.transaction` → one-line description (Stellar docs). */
28
+ const HORIZON_TRANSACTION_RESULT_CODE_DOCUMENTATION: Record<string, string> = {
29
+ tx_success: 'The transaction succeeded.',
30
+ tx_failed: 'One of the operations failed (none were applied).',
31
+ tx_too_early: 'The ledger closeTime was before the minTime.',
32
+ tx_too_late: 'The ledger closeTime was after the maxTime.',
33
+ tx_missing_operation: 'No operation was specified.',
34
+ tx_bad_seq: 'Sequence number does not match source account.',
35
+ tx_bad_auth: 'Too few valid signatures / wrong network.',
36
+ tx_insufficient_balance: 'Fee would bring account below reserve.',
37
+ tx_no_source_account: 'Source account not found.',
38
+ tx_insufficient_fee: 'Fee is too small.',
39
+ tx_bad_auth_extra: 'Unused signatures attached to transaction.',
40
+ tx_internal_error: 'An unknown error occurred.',
41
+ tx_fee_bump_inner_success: 'Fee bump inner transaction succeeded.',
42
+ tx_fee_bump_inner_failed: 'Fee bump inner transaction failed.',
43
+ tx_not_supported: 'Transaction type not supported.',
44
+ tx_bad_sponsorship: 'Sponsorship is invalid.',
45
+ tx_bad_min_seq_age_or_gap: 'Minimum sequence age or gap precondition failed.',
46
+ tx_malformed: 'Transaction is malformed.',
47
+ tx_soroban_invalid: 'Soroban transaction is invalid.',
48
+ }
49
+
50
+ /** Horizon `extras.result_codes.operations[]` → one-line description (Stellar docs). */
51
+ const HORIZON_OPERATION_RESULT_CODE_DOCUMENTATION: Record<string, string> = {
52
+ op_inner: 'The inner object result is valid and the operation was a success.',
53
+ op_bad_auth:
54
+ 'There are too few valid signatures, or the transaction was submitted to the wrong network.',
55
+ op_no_source_account: 'The source account was not found.',
56
+ op_no_account: 'The source account was not found.',
57
+ op_not_supported: 'The operation is not supported at this time.',
58
+ op_too_many_subentries: 'Max number of subentries (1000) already reached.',
59
+ op_exceeded_work_limit: 'Operation did too much work.',
60
+ op_malformed: 'The operation input is invalid.',
61
+ op_underfunded:
62
+ 'The source account does not have enough balance to send the payment amount while maintaining its minimum reserve.',
63
+ op_src_no_trust:
64
+ 'The source account does not have a trustline for the asset it is trying to send.',
65
+ op_src_not_authorized: 'The source account is not authorized to send this asset.',
66
+ op_no_destination: 'The destination account does not exist.',
67
+ op_no_trust: 'The destination account does not have a trustline for the asset being sent.',
68
+ op_not_authorized: 'The destination account is not authorized to hold this asset.',
69
+ op_line_full:
70
+ 'The destination account does not have sufficient limits to receive the amount and still satisfy its buying liabilities.',
71
+ op_no_issuer: 'The issuer of the asset does not exist.',
72
+ }
73
+
74
+ /** Operation result codes that mark success for a prior op in a multi-op failure list. */
75
+ const HORIZON_OPERATION_SUCCESS_RESULT_CODES = new Set<string>(['op_inner'])
76
+
77
+ function failingOperationCode(operations: string[] | undefined): string | undefined {
78
+ return operations?.find((code) => !HORIZON_OPERATION_SUCCESS_RESULT_CODES.has(code))
79
+ }
80
+
81
+ export function isHorizonErrorBody(data: unknown): data is HorizonErrorBody {
82
+ return !!(
83
+ data &&
84
+ typeof data === 'object' &&
85
+ 'title' in data &&
86
+ typeof data.title === 'string' &&
87
+ 'status' in data &&
88
+ typeof data.status === 'number'
89
+ )
90
+ }
91
+
92
+ /**
93
+ * Horizon failures are usually {@link BadResponseError} with `response` = problem+json body.
94
+ * Axios rejects first; the SDK forwards it unchanged, so the body lives on `error.response.data`.
95
+ */
96
+ export function getHorizonErrorBody(error: unknown): HorizonErrorBody | null {
97
+ if (error instanceof BadResponseError && isHorizonErrorBody(error.response)) {
98
+ return error.response
99
+ }
100
+ if (error && typeof error === 'object' && 'response' in error) {
101
+ const data = (error as { response?: { data?: unknown } }).response?.data
102
+ if (isHorizonErrorBody(data)) {
103
+ return data
104
+ }
105
+ }
106
+ return null
107
+ }
108
+
109
+ function lookupOperationDocumentation(operationCode: string | undefined): string | undefined {
110
+ if (!operationCode) {
111
+ return undefined
112
+ }
113
+ return HORIZON_OPERATION_RESULT_CODE_DOCUMENTATION[operationCode]
114
+ }
115
+
116
+ function lookupTransactionDocumentation(transactionCode: string | undefined): string | undefined {
117
+ if (!transactionCode) {
118
+ return undefined
119
+ }
120
+ return HORIZON_TRANSACTION_RESULT_CODE_DOCUMENTATION[transactionCode]
121
+ }
122
+
123
+ function detailDocumentation(body: HorizonErrorBody): string | undefined {
124
+ const detail = body.detail?.trim()
125
+ if (!detail || GENERIC_TRANSACTION_FAILED_DETAIL.test(detail)) {
126
+ return undefined
127
+ }
128
+ return detail
129
+ }
130
+
131
+ function extrasReasonDocumentation(body: HorizonErrorBody): string | undefined {
132
+ const reason = body.extras?.reason?.trim()
133
+ if (!reason) {
134
+ return undefined
135
+ }
136
+ const invalidField = body.extras?.invalid_field?.trim()
137
+ return invalidField ? `${reason} (field: ${invalidField})` : reason
138
+ }
139
+
140
+ /**
141
+ * Best-effort human-readable summary for logs (`errorExtras.documentationSummary`).
142
+ * Prefers operation result codes, then transaction codes, then Horizon `detail` / `extras.reason`.
143
+ */
144
+ export function documentationSummaryFromHorizonBody(body: HorizonErrorBody): string {
145
+ const transactionCode = body.extras?.result_codes?.transaction
146
+ const operationCode = failingOperationCode(body.extras?.result_codes?.operations)
147
+
148
+ const operationSummary = lookupOperationDocumentation(operationCode)
149
+ if (operationSummary) {
150
+ return operationSummary
151
+ }
152
+
153
+ const transactionSummary = lookupTransactionDocumentation(transactionCode)
154
+ if (transactionSummary && transactionCode !== 'tx_failed') {
155
+ return transactionSummary
156
+ }
157
+
158
+ const detail = detailDocumentation(body)
159
+ if (detail) {
160
+ return detail
161
+ }
162
+
163
+ const extrasReason = extrasReasonDocumentation(body)
164
+ if (extrasReason) {
165
+ return extrasReason
166
+ }
167
+
168
+ if (transactionSummary) {
169
+ return transactionSummary
170
+ }
171
+
172
+ const title = body.title?.trim()
173
+ if (title) {
174
+ return title
175
+ }
176
+
177
+ return 'Unknown Horizon error.'
178
+ }
@@ -1,112 +1,184 @@
1
- import { LedgerAPI4xx, LedgerAPI5xx, NetworkDown } from "@ledgerhq/errors";
2
- import { NetworkError, NotFoundError } from "@stellar/stellar-sdk";
1
+ // SPDX-FileCopyrightText: © 2026 LEDGER SAS
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { LedgerAPI4xx, LedgerAPI5xx, NetworkDown } from '@ledgerhq/errors'
5
+ import { NetworkError, NotFoundError } from '@stellar/stellar-sdk'
3
6
  import {
4
7
  messageFromHorizonUnknown,
5
8
  throwHorizonLedgerOrOperationsError,
6
- } from "./horizonLedgerErrors";
9
+ } from './horizonLedgerErrors'
10
+
11
+ describe('messageFromHorizonUnknown', () => {
12
+ it('returns Horizon documentationSummary when response body is present', () => {
13
+ const error = Object.assign(new Error('Request failed with status code 400'), {
14
+ response: {
15
+ data: {
16
+ title: 'Transaction Malformed',
17
+ status: 400,
18
+ detail: 'Horizon could not decode the transaction envelope in this request.',
19
+ },
20
+ },
21
+ })
22
+ expect(messageFromHorizonUnknown(error)).toBe(
23
+ 'Horizon could not decode the transaction envelope in this request.'
24
+ )
25
+ })
7
26
 
8
- describe("messageFromHorizonUnknown", () => {
9
- it("returns message for Error", () => {
10
- expect(messageFromHorizonUnknown(new Error("hello"))).toBe("hello");
11
- });
27
+ it('returns message for Error', () => {
28
+ expect(messageFromHorizonUnknown(new Error('hello'))).toBe('hello')
29
+ })
12
30
 
13
- it("returns string as-is", () => {
14
- expect(messageFromHorizonUnknown("plain")).toBe("plain");
15
- });
31
+ it('returns string as-is', () => {
32
+ expect(messageFromHorizonUnknown('plain')).toBe('plain')
33
+ })
16
34
 
17
- it("returns empty string for non-Error non-string", () => {
18
- expect(messageFromHorizonUnknown({ code: 1 })).toBe("");
19
- expect(messageFromHorizonUnknown(null)).toBe("");
20
- expect(messageFromHorizonUnknown(undefined)).toBe("");
21
- });
22
- });
35
+ it('returns empty string for non-Error non-string', () => {
36
+ expect(messageFromHorizonUnknown({ code: 1 })).toBe('')
37
+ expect(messageFromHorizonUnknown(null)).toBe('')
38
+ expect(messageFromHorizonUnknown(undefined)).toBe('')
39
+ })
40
+ })
23
41
 
24
- describe("throwHorizonLedgerOrOperationsError", () => {
25
- const notFound = "ledger not found";
42
+ describe('throwHorizonLedgerOrOperationsError', () => {
43
+ const notFound = 'ledger not found'
26
44
 
27
- it("throws notFoundMessage for NotFoundError", () => {
45
+ it('throws notFoundMessage for NotFoundError', () => {
28
46
  expect(() =>
29
- throwHorizonLedgerOrOperationsError(new NotFoundError("gone", {}), notFound),
30
- ).toThrow(notFound);
31
- });
47
+ throwHorizonLedgerOrOperationsError(new NotFoundError('gone', {}), notFound)
48
+ ).toThrow(notFound)
49
+ })
32
50
 
33
- it("throws notFoundMessage when message contains status code 404", () => {
51
+ it('throws notFoundMessage when message contains status code 404', () => {
34
52
  expect(() =>
35
- throwHorizonLedgerOrOperationsError(new Error("Request failed: status code 404"), notFound),
36
- ).toThrow(notFound);
37
- });
53
+ throwHorizonLedgerOrOperationsError(new Error('Request failed: status code 404'), notFound)
54
+ ).toThrow(notFound)
55
+ })
38
56
 
39
- it("throws LedgerAPI4xx 429 for too many requests in message", () => {
57
+ it('throws notFoundMessage when Horizon problem body has status 404', () => {
58
+ const error = Object.assign(new Error('Request failed with status code 404'), {
59
+ response: {
60
+ data: {
61
+ title: 'Resource Missing',
62
+ status: 404,
63
+ detail: 'The resource at the url requested was not found.',
64
+ },
65
+ },
66
+ })
67
+ expect(() => throwHorizonLedgerOrOperationsError(error, notFound)).toThrow(notFound)
68
+ })
69
+
70
+ it('throws LedgerAPI4xx 429 for too many requests in message', () => {
40
71
  expect(() =>
41
- throwHorizonLedgerOrOperationsError(new Error("Too Many Requests"), notFound),
42
- ).toThrow(LedgerAPI4xx);
43
- });
72
+ throwHorizonLedgerOrOperationsError(new Error('Too Many Requests'), notFound)
73
+ ).toThrow(LedgerAPI4xx)
74
+ })
75
+
76
+ it('throws LedgerAPI4xx 429 when Horizon problem body has status 429', () => {
77
+ const error = Object.assign(new Error('Request failed with status code 429'), {
78
+ response: {
79
+ data: {
80
+ title: 'Rate Limited',
81
+ status: 429,
82
+ detail: 'The request was rate limited.',
83
+ },
84
+ },
85
+ })
86
+ expect(() => throwHorizonLedgerOrOperationsError(error, notFound)).toThrow(
87
+ expect.objectContaining({ name: 'LedgerAPI4xx', status: 429 })
88
+ )
89
+ })
44
90
 
45
- it("throws LedgerAPI4xx for other 4xx status in message", () => {
91
+ it('throws LedgerAPI4xx for other 4xx status in message', () => {
46
92
  expect(() =>
47
- throwHorizonLedgerOrOperationsError(new Error("status code 400"), notFound),
48
- ).toThrow(LedgerAPI4xx);
49
- });
93
+ throwHorizonLedgerOrOperationsError(new Error('status code 400'), notFound)
94
+ ).toThrow(LedgerAPI4xx)
95
+ })
50
96
 
51
- test.each([["401"], ["403"]])(
52
- "throws LedgerAPI4xx when message contains status code %s",
53
- code => {
97
+ it('throws LedgerAPI4xx when Horizon problem body has status 400', () => {
98
+ const error = Object.assign(new Error('Request failed with status code 400'), {
99
+ response: {
100
+ data: {
101
+ title: 'Bad Request',
102
+ status: 400,
103
+ detail: 'The request you sent was invalid in some way.',
104
+ },
105
+ },
106
+ })
107
+ expect(() => throwHorizonLedgerOrOperationsError(error, notFound)).toThrow(LedgerAPI4xx)
108
+ })
109
+
110
+ test.each([['401'], ['403']])(
111
+ 'throws LedgerAPI4xx when message contains status code %s',
112
+ (code) => {
54
113
  expect(() =>
55
114
  throwHorizonLedgerOrOperationsError(
56
115
  new Error(`Request failed: status code ${code}`),
57
- notFound,
58
- ),
59
- ).toThrow(LedgerAPI4xx);
60
- },
61
- );
116
+ notFound
117
+ )
118
+ ).toThrow(LedgerAPI4xx)
119
+ }
120
+ )
62
121
 
63
- it("throws LedgerAPI5xx for 5xx status in message", () => {
122
+ it('throws LedgerAPI5xx for 5xx status in message', () => {
64
123
  expect(() =>
65
- throwHorizonLedgerOrOperationsError(new Error("status code 503"), notFound),
66
- ).toThrow(LedgerAPI5xx);
67
- });
124
+ throwHorizonLedgerOrOperationsError(new Error('status code 503'), notFound)
125
+ ).toThrow(LedgerAPI5xx)
126
+ })
68
127
 
69
- test.each([["502"], ["500"]])(
70
- "throws LedgerAPI5xx when message contains status code %s",
71
- code => {
128
+ it('throws LedgerAPI5xx when Horizon problem body has status 503', () => {
129
+ const error = Object.assign(new Error('Request failed with status code 503'), {
130
+ response: {
131
+ data: {
132
+ title: 'Internal Server Error',
133
+ status: 503,
134
+ detail: 'An error occurred while processing this request.',
135
+ },
136
+ },
137
+ })
138
+ expect(() => throwHorizonLedgerOrOperationsError(error, notFound)).toThrow(LedgerAPI5xx)
139
+ })
140
+
141
+ test.each([['502'], ['500']])(
142
+ 'throws LedgerAPI5xx when message contains status code %s',
143
+ (code) => {
72
144
  expect(() =>
73
- throwHorizonLedgerOrOperationsError(new Error(`upstream status code ${code}`), notFound),
74
- ).toThrow(LedgerAPI5xx);
75
- },
76
- );
145
+ throwHorizonLedgerOrOperationsError(new Error(`upstream status code ${code}`), notFound)
146
+ ).toThrow(LedgerAPI5xx)
147
+ }
148
+ )
77
149
 
78
- it("throws NetworkDown for NetworkError", () => {
150
+ it('throws NetworkDown for NetworkError', () => {
79
151
  expect(() =>
80
- throwHorizonLedgerOrOperationsError(new NetworkError("net", {}), notFound),
81
- ).toThrow(NetworkDown);
82
- });
152
+ throwHorizonLedgerOrOperationsError(new NetworkError('net', {}), notFound)
153
+ ).toThrow(NetworkDown)
154
+ })
83
155
 
84
156
  test.each([
85
- ["ECONNRESET", "ECONNRESET on read"],
86
- ["ECONNREFUSED", "connect ECONNREFUSED 127.0.0.1:443"],
87
- ["ENOTFOUND", "getaddrinfo ENOTFOUND horizon.example"],
88
- ["EPIPE", "write EPIPE broken pipe"],
89
- ["ETIMEDOUT", "connect ETIMEDOUT 10.0.0.1:443"],
90
- ])("throws NetworkDown when message contains %s", (_label, message) => {
157
+ ['ECONNRESET', 'ECONNRESET on read'],
158
+ ['ECONNREFUSED', 'connect ECONNREFUSED 127.0.0.1:443'],
159
+ ['ENOTFOUND', 'getaddrinfo ENOTFOUND horizon.example'],
160
+ ['EPIPE', 'write EPIPE broken pipe'],
161
+ ['ETIMEDOUT', 'connect ETIMEDOUT 10.0.0.1:443'],
162
+ ])('throws NetworkDown when message contains %s', (_label, message) => {
91
163
  expect(() => throwHorizonLedgerOrOperationsError(new Error(message), notFound)).toThrow(
92
- NetworkDown,
93
- );
94
- });
164
+ NetworkDown
165
+ )
166
+ })
95
167
 
96
- it("throws NetworkDown for undefined is not an object in message", () => {
168
+ it('throws NetworkDown for undefined is not an object in message', () => {
97
169
  expect(() =>
98
- throwHorizonLedgerOrOperationsError(new Error("undefined is not an object"), notFound),
99
- ).toThrow(NetworkDown);
100
- });
170
+ throwHorizonLedgerOrOperationsError(new Error('undefined is not an object'), notFound)
171
+ ).toThrow(NetworkDown)
172
+ })
101
173
 
102
- it("rethrows unknown errors", () => {
103
- const o = { custom: true };
174
+ it('rethrows unknown errors', () => {
175
+ const o = { custom: true }
104
176
  try {
105
- throwHorizonLedgerOrOperationsError(o, notFound);
177
+ throwHorizonLedgerOrOperationsError(o, notFound)
106
178
  } catch (e) {
107
- expect(e).toBe(o);
108
- return;
179
+ expect(e).toBe(o)
180
+ return
109
181
  }
110
- throw new Error("expected throw");
111
- });
112
- });
182
+ throw new Error('expected throw')
183
+ })
184
+ })
@@ -1,45 +1,72 @@
1
- import { LedgerAPI4xx, LedgerAPI5xx, NetworkDown } from "@ledgerhq/errors";
2
- import { NetworkError, NotFoundError } from "@stellar/stellar-sdk";
1
+ // SPDX-FileCopyrightText: © 2026 LEDGER SAS
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { LedgerAPI4xx, LedgerAPI5xx, NetworkDown } from '@ledgerhq/errors'
5
+ import { NetworkError, NotFoundError } from '@stellar/stellar-sdk'
6
+ import { documentationSummaryFromHorizonBody, getHorizonErrorBody } from './horizonErrorBody'
3
7
 
4
8
  export function messageFromHorizonUnknown(e: unknown): string {
9
+ const body = getHorizonErrorBody(e)
10
+ if (body) {
11
+ return documentationSummaryFromHorizonBody(body)
12
+ }
5
13
  if (e instanceof Error) {
6
- return e.message;
14
+ return e.message
7
15
  }
8
- if (typeof e === "string") {
9
- return e;
16
+ if (typeof e === 'string') {
17
+ return e
10
18
  }
11
- return "";
19
+ return ''
12
20
  }
13
21
 
14
- const HORIZON_STATUS_404 = /status code 404/;
15
- const HORIZON_TOO_MANY_REQUESTS = /too many requests/i;
16
- const HORIZON_STATUS_4XX = /status code 4\d{2}/;
17
- const HORIZON_STATUS_5XX = /status code 5\d{2}/;
18
- const HORIZON_NETWORK_DOWN = /ECONNRESET|ECONNREFUSED|ENOTFOUND|EPIPE|ETIMEDOUT/;
19
- const HORIZON_UNDEFINED_OBJECT = /undefined is not an object/;
22
+ const HORIZON_STATUS_404 = /status code 404/
23
+ const HORIZON_TOO_MANY_REQUESTS = /too many requests/i
24
+ const HORIZON_STATUS_4XX = /status code 4\d{2}/
25
+ const HORIZON_STATUS_5XX = /status code 5\d{2}/
26
+ const HORIZON_NETWORK_DOWN = /ECONNRESET|ECONNREFUSED|ENOTFOUND|EPIPE|ETIMEDOUT/
27
+ const HORIZON_UNDEFINED_OBJECT = /undefined is not an object/
20
28
 
21
29
  export function throwHorizonLedgerOrOperationsError(e: unknown, notFoundMessage: string): never {
22
- const errorMsg = messageFromHorizonUnknown(e);
30
+ if (e instanceof NotFoundError) {
31
+ throw new Error(notFoundMessage)
32
+ }
33
+
34
+ const body = getHorizonErrorBody(e)
35
+ const status = body?.status
36
+ if (status === 404) {
37
+ throw new Error(notFoundMessage)
38
+ }
39
+ if (status === 429) {
40
+ throw new LedgerAPI4xx('status code 4xx', { status: 429, url: undefined, method: 'GET' })
41
+ }
42
+ if (status !== undefined && status >= 400 && status < 500) {
43
+ throw new LedgerAPI4xx()
44
+ }
45
+ if (status !== undefined && status >= 500 && status < 600) {
46
+ throw new LedgerAPI5xx()
47
+ }
48
+
49
+ const errorMsg = messageFromHorizonUnknown(e)
23
50
 
24
- if (e instanceof NotFoundError || HORIZON_STATUS_404.exec(errorMsg)) {
25
- throw new Error(notFoundMessage);
51
+ if (HORIZON_STATUS_404.exec(errorMsg)) {
52
+ throw new Error(notFoundMessage)
26
53
  }
27
54
  if (HORIZON_TOO_MANY_REQUESTS.exec(errorMsg)) {
28
- throw new LedgerAPI4xx("status code 4xx", { status: 429, url: undefined, method: "GET" });
55
+ throw new LedgerAPI4xx('status code 4xx', { status: 429, url: undefined, method: 'GET' })
29
56
  }
30
57
  if (HORIZON_STATUS_4XX.exec(errorMsg)) {
31
- throw new LedgerAPI4xx();
58
+ throw new LedgerAPI4xx()
32
59
  }
33
60
  if (HORIZON_STATUS_5XX.exec(errorMsg)) {
34
- throw new LedgerAPI5xx();
61
+ throw new LedgerAPI5xx()
35
62
  }
36
63
  if (
37
64
  e instanceof NetworkError ||
38
65
  HORIZON_NETWORK_DOWN.exec(errorMsg) ||
39
66
  HORIZON_UNDEFINED_OBJECT.exec(errorMsg)
40
67
  ) {
41
- throw new NetworkDown();
68
+ throw new NetworkDown()
42
69
  }
43
70
 
44
- throw e;
71
+ throw e
45
72
  }
@@ -1,3 +1,6 @@
1
+ // SPDX-FileCopyrightText: © 2026 LEDGER SAS
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
1
4
  export {
2
5
  broadcastTransaction,
3
6
  fetchAccount,
@@ -13,4 +16,4 @@ export {
13
16
  getRecipientAccount,
14
17
  loadAccount,
15
18
  registerHorizonInterceptors,
16
- } from "./horizon";
19
+ } from './horizon'