Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 46 additions & 4 deletions src/errorSuggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
PaymentExceedsRemainingError,
InvoiceFrozenError,
CoCreatorApprovalNotRequiredError,
SdkError,
SdkErrorCode,
} from "./errors.js";

type ErrorConstructor = new (...args: never[]) => StellarSplitError;
Expand Down Expand Up @@ -51,6 +53,35 @@ const SUGGESTION_TABLE: SuggestionEntry[] = [
},
];

// === Machine-readable error-code suggestions

// Keyed by SdkErrorCode. Consulted when getSuggestion is called with a code
// string or an SdkError instance, ahead of the Error-subclass and raw-pattern
// tables. Lookup normalizes the key with toUpperCase so a caller passing
// "account_not_found" resolves the same entry as "ACCOUNT_NOT_FOUND".
const CODE_SUGGESTION_TABLE: Record<SdkErrorCode, string> = {
[SdkErrorCode.INVOICE_NOT_FOUND]:
"The requested invoice does not exist on-chain. Verify the invoice ID and ensure it was created on the correct network.",
[SdkErrorCode.ACCOUNT_NOT_FOUND]:
"The account does not exist on the Stellar network. Fund the account with a minimum XLM balance before retrying.",
[SdkErrorCode.INSUFFICIENT_FUNDS]:
"The account balance is too low to cover this operation. Top up the account and retry.",
[SdkErrorCode.DEADLINE_EXPIRED]:
"The invoice deadline has passed. Create a new invoice with a future deadline if you still need to collect payment.",
[SdkErrorCode.INVALID_RECIPIENT]:
"One or more recipient addresses are invalid. Check that each recipient is a valid Stellar public key.",
[SdkErrorCode.CONTRACT_REJECTED]:
"The contract rejected the transaction. Review the operation parameters and the invoice state before retrying.",
[SdkErrorCode.NETWORK_TIMEOUT]:
"The network request timed out. Check your connection and retry; the transaction may still settle.",
[SdkErrorCode.RATE_LIMITED]:
"Requests are being rate limited. Back off and retry after a short delay.",
};

function suggestionForCode(code: string): string {
return CODE_SUGGESTION_TABLE[code.toUpperCase() as SdkErrorCode] ?? GENERIC_FALLBACK;
}

// Additional raw-message pattern suggestions for contract-level errors surfaced
// through parseSorobanError as generic StellarSplitErrors.
const RAW_PATTERN_TABLE: Array<{ pattern: RegExp; suggestion: string }> = [
Expand Down Expand Up @@ -79,13 +110,24 @@ const RAW_PATTERN_TABLE: Array<{ pattern: RegExp; suggestion: string }> = [
/**
* Returns a human-readable remediation suggestion for a known SDK error.
*
* Matches first against typed error subclasses, then against raw message
* patterns, and falls back to a generic suggestion for unknown errors.
* Accepts either a machine-readable code (an {@link SdkErrorCode} or any
* string, matched case-insensitively) or an Error instance. Codes and
* {@link SdkError} instances are resolved against the code table; other errors
* match first against typed subclasses, then against raw message patterns,
* and fall back to a generic suggestion.
*
* @param error - Any Error instance, ideally a StellarSplitError subclass.
* @param error - An SdkErrorCode, a code string, or any Error instance.
* @returns A suggestion string suitable for display in UI or logs.
*/
export function getSuggestion(error: Error): string {
export function getSuggestion(error: Error | SdkErrorCode | string): string {
if (typeof error === "string") {
return suggestionForCode(error);
}

if (error instanceof SdkError) {
return suggestionForCode(error.code);
}

for (const entry of SUGGESTION_TABLE) {
if (error instanceof entry.type) {
return entry.suggestion;
Expand Down
1 change: 1 addition & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2100,6 +2100,7 @@ export class ChannelExhaustedError extends StellarSplitError {
/** Machine-readable error codes carried by {@link SdkError}. */
export enum SdkErrorCode {
INVOICE_NOT_FOUND = "INVOICE_NOT_FOUND",
ACCOUNT_NOT_FOUND = "ACCOUNT_NOT_FOUND",
INSUFFICIENT_FUNDS = "INSUFFICIENT_FUNDS",
DEADLINE_EXPIRED = "DEADLINE_EXPIRED",
INVALID_RECIPIENT = "INVALID_RECIPIENT",
Expand Down
34 changes: 34 additions & 0 deletions test/errorSuggestions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ import {
PaymentExceedsRemainingError,
InvoiceFrozenError,
CoCreatorApprovalNotRequiredError,
SdkError,
SdkErrorCode,
} from "../src/errors.js";

const ACCOUNT_NOT_FOUND_SUGGESTION =
"The account does not exist on the Stellar network. Fund the account with a minimum XLM balance before retrying.";

describe("getSuggestion — typed error classes", () => {
it("returns suggestion for InvoiceNotFoundError", () => {
const s = getSuggestion(new InvoiceNotFoundError("inv-1"));
Expand Down Expand Up @@ -68,6 +73,35 @@ describe("getSuggestion — raw-message pattern matching", () => {
});
});

describe("getSuggestion — machine-readable error codes", () => {
it("returns the account-specific suggestion for the ACCOUNT_NOT_FOUND code", () => {
expect(getSuggestion("ACCOUNT_NOT_FOUND")).toBe(ACCOUNT_NOT_FOUND_SUGGESTION);
});

it("normalizes code casing before lookup", () => {
expect(getSuggestion("account_not_found")).toBe(ACCOUNT_NOT_FOUND_SUGGESTION);
});

it("resolves an SdkErrorCode enum value", () => {
expect(getSuggestion(SdkErrorCode.ACCOUNT_NOT_FOUND)).toBe(ACCOUNT_NOT_FOUND_SUGGESTION);
});

it("resolves an SdkError instance via its code", () => {
const err = new SdkError("account missing", SdkErrorCode.ACCOUNT_NOT_FOUND);
expect(getSuggestion(err)).toBe(ACCOUNT_NOT_FOUND_SUGGESTION);
});

it("does not affect other error-code suggestions", () => {
expect(getSuggestion("INVOICE_NOT_FOUND")).toContain("does not exist on-chain");
expect(getSuggestion("RATE_LIMITED")).toContain("rate limited");
expect(getSuggestion("INVOICE_NOT_FOUND")).not.toBe(ACCOUNT_NOT_FOUND_SUGGESTION);
});

it("falls back to the generic suggestion for an unknown code", () => {
expect(getSuggestion("NOPE_NOT_A_CODE")).toContain("unexpected error");
});
});

describe("getSuggestion — fallback for unknown errors", () => {
it("returns generic fallback for an unknown Error", () => {
const s = getSuggestion(new Error("something weird happened"));
Expand Down