From f84b82d0bba0157da3d4f6b91858229b7a72720e Mon Sep 17 00:00:00 2001 From: afurious Date: Thu, 27 Aug 2026 15:48:04 +0100 Subject: [PATCH] fix: resolve ACCOUNT_NOT_FOUND suggestion via typed error-code lookup --- src/errorSuggestions.ts | 50 ++++++++++++++++++++++++++++++++--- src/errors.ts | 1 + test/errorSuggestions.test.ts | 34 ++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/errorSuggestions.ts b/src/errorSuggestions.ts index 4585e84..9cf2d28 100644 --- a/src/errorSuggestions.ts +++ b/src/errorSuggestions.ts @@ -6,6 +6,8 @@ import { PaymentExceedsRemainingError, InvoiceFrozenError, CoCreatorApprovalNotRequiredError, + SdkError, + SdkErrorCode, } from "./errors.js"; type ErrorConstructor = new (...args: never[]) => StellarSplitError; @@ -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.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 }> = [ @@ -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; diff --git a/src/errors.ts b/src/errors.ts index 02e444b..fd2452d 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -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", diff --git a/test/errorSuggestions.test.ts b/test/errorSuggestions.test.ts index c48c688..f6a3c23 100644 --- a/test/errorSuggestions.test.ts +++ b/test/errorSuggestions.test.ts @@ -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")); @@ -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"));