@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.
- package/CHANGELOG.md +6 -0
- package/lib/network/horizon.d.ts.map +1 -1
- package/lib/network/horizon.js +3 -53
- package/lib/network/horizon.js.map +1 -1
- package/lib/network/horizonErrorBody.d.ts +29 -0
- package/lib/network/horizonErrorBody.d.ts.map +1 -0
- package/lib/network/horizonErrorBody.js +139 -0
- package/lib/network/horizonErrorBody.js.map +1 -0
- package/lib/network/horizonLedgerErrors.d.ts.map +1 -1
- package/lib/network/horizonLedgerErrors.js +23 -1
- package/lib/network/horizonLedgerErrors.js.map +1 -1
- package/lib-es/network/horizon.d.ts.map +1 -1
- package/lib-es/network/horizon.js +4 -54
- package/lib-es/network/horizon.js.map +1 -1
- package/lib-es/network/horizonErrorBody.d.ts +29 -0
- package/lib-es/network/horizonErrorBody.d.ts.map +1 -0
- package/lib-es/network/horizonErrorBody.js +134 -0
- package/lib-es/network/horizonErrorBody.js.map +1 -0
- package/lib-es/network/horizonLedgerErrors.d.ts.map +1 -1
- package/lib-es/network/horizonLedgerErrors.js +23 -1
- package/lib-es/network/horizonLedgerErrors.js.map +1 -1
- package/package.json +2 -2
- package/src/api/index.integ.test.ts +4 -2
- package/src/network/broadcastTransaction.test.ts +20 -0
- package/src/network/horizon.ts +8 -76
- package/src/network/horizon.unit.test.ts +2 -2
- package/src/network/horizonErrorBody.test.ts +133 -0
- package/src/network/horizonErrorBody.ts +178 -0
- package/src/network/horizonLedgerErrors.test.ts +69 -0
- package/src/network/horizonLedgerErrors.ts +25 -1
|
@@ -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":"
|
|
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 (
|
|
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;
|
|
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.
|
|
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.
|
|
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
|
|
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:
|
|
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)
|
package/src/network/horizon.ts
CHANGED
|
@@ -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:
|
|
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 =
|
|
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
|
|
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: '
|
|
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
|
+
})
|