diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz
index 55cff370..15962664 100644
Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ
diff --git a/packages/ui/src/Common/Toast/Toast.stories.tsx b/packages/ui/src/Common/Toast/Toast.stories.tsx
index bb0b53c8..3aee3797 100644
--- a/packages/ui/src/Common/Toast/Toast.stories.tsx
+++ b/packages/ui/src/Common/Toast/Toast.stories.tsx
@@ -53,6 +53,58 @@ Warning.args = {
onClose: () => console.log("Toast closed"),
};
+export const ErrorWithEnhancedDetails = SingleToastTemplate.bind({});
+ErrorWithEnhancedDetails.args = {
+ id: "enhanced-error",
+ type: "error",
+ title: "Contract Error",
+ message:
+ "The slippage exceeds the allowable limit. In Phase 1 of the launch, it's capped at 1%. Please try to swap a lower amount of tokens.",
+ onClose: () => console.log("Toast closed"),
+ // Simulate enhanced error object from the new error resolver
+ error: {
+ message: "Transaction simulation failed: HostError: Error(Contract, #300)",
+ userFriendlyMessage:
+ "The slippage exceeds the allowable limit. In Phase 1 of the launch, it's capped at 1%. Please try to swap a lower amount of tokens.",
+ errorCode: 300,
+ contractType: "pair",
+ stack:
+ "Error: Transaction simulation failed: HostError: Error(Contract, #300)\n at PhoenixPairContract.swap (contract.js:123:15)\n at executeContractTransaction (useContractTransaction.tsx:45:20)",
+ },
+};
+
+export const ErrorWithStakeContract = SingleToastTemplate.bind({});
+ErrorWithStakeContract.args = {
+ id: "stake-error",
+ type: "error",
+ title: "Staking Error",
+ message: "Insufficient staking balance",
+ onClose: () => console.log("Toast closed"),
+ error: {
+ message: "Error(Contract, #503)",
+ userFriendlyMessage: "Insufficient staking balance",
+ errorCode: 503,
+ contractType: "stake",
+ },
+};
+
+export const ErrorWithUnknownCode = SingleToastTemplate.bind({});
+ErrorWithUnknownCode.args = {
+ id: "unknown-error",
+ type: "error",
+ title: "Unknown Contract Error",
+ message:
+ "Contract error occurred (code: 999). Please try again or contact support.",
+ onClose: () => console.log("Toast closed"),
+ error: {
+ message: "Error(Contract, #999)",
+ userFriendlyMessage:
+ "Contract error occurred (code: 999). Please try again or contact support.",
+ errorCode: 999,
+ contractType: null,
+ },
+};
+
export const Info = SingleToastTemplate.bind({});
Info.args = {
id: "4",
diff --git a/packages/ui/src/Common/Toast/Toast.tsx b/packages/ui/src/Common/Toast/Toast.tsx
index 78b75466..605e2169 100644
--- a/packages/ui/src/Common/Toast/Toast.tsx
+++ b/packages/ui/src/Common/Toast/Toast.tsx
@@ -6,8 +6,8 @@ import {
CircularProgress,
Link,
Collapse,
+ Button,
} from "@mui/material";
-import { motion } from "framer-motion";
import CloseIcon from "@mui/icons-material/Close";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import ErrorIcon from "@mui/icons-material/Error";
@@ -16,6 +16,7 @@ import WarningIcon from "@mui/icons-material/Warning";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import LaunchIcon from "@mui/icons-material/Launch";
+import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import {
colors,
typography,
@@ -34,7 +35,16 @@ export interface ToastProps {
onClose: (id: string) => void;
autoHideDuration?: number;
transactionId?: string;
- error?: Error | string | { message: string; stack?: string }; // For collapsible error details
+ error?:
+ | Error
+ | string
+ | {
+ message?: string;
+ stack?: string;
+ userFriendlyMessage?: string;
+ errorCode?: number | null;
+ contractType?: string | null;
+ }; // For collapsible error details with enhanced error resolver support
}
const getToastIcon = (type: ToastType) => {
@@ -48,7 +58,20 @@ const getToastIcon = (type: ToastType) => {
case "info":
return ;
case "loading":
- return ;
+ return (
+
+ );
default:
return ;
}
@@ -70,6 +93,22 @@ const getToastColor = (type: ToastType) => {
}
};
+// Helper function to convert hex color to rgb values for gradient usage
+const hexToRgb = (hex: string) => {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result
+ ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(
+ result[3],
+ 16
+ )}`
+ : "249, 115, 22"; // Fallback to primary color RGB
+};
+
+// Helper function to get RGB from toast color
+const getToastColorRgb = (type: ToastType) => {
+ return hexToRgb(getToastColor(type));
+};
+
// Helper function to get Explorer URL for transaction
const getExplorerUrl = (transactionId: string) => {
return `https://stellar.expert/explorer/public/tx/${transactionId}`;
@@ -81,7 +120,7 @@ export const Toast = ({
title,
message,
onClose,
- autoHideDuration = 5000,
+ autoHideDuration = type === "error" ? 10000 : 5000, // Error toasts stay 10 seconds, others 5 seconds
transactionId,
error,
}: ToastProps) => {
@@ -101,92 +140,217 @@ export const Toast = ({
}, [id, onClose, autoHideDuration, type]);
// Format error details for display
- const errorMessage = React.useMemo(() => {
- if (!error) return "";
- if (typeof error === "string") return error;
+ const errorDetails = React.useMemo(() => {
+ if (!error) return { display: "", technical: "", hasTechnical: false };
+
+ if (typeof error === "string") {
+ return { display: error, technical: error, hasTechnical: false };
+ }
- // Handle Error objects and plain objects with message/stack properties
+ // Handle enhanced error objects with user-friendly messages
const errorObj = error as any;
- return errorObj.stack || errorObj.message || String(error);
+
+ // If we have enhanced error details, format them nicely
+ if (errorObj.userFriendlyMessage && errorObj.errorCode !== undefined) {
+ const technicalInfo = [
+ `Error Code: ${errorObj.errorCode}`,
+ errorObj.contractType
+ ? `Contract Type: ${errorObj.contractType}`
+ : null,
+ `Technical Details: ${
+ errorObj.message || errorObj.stack || "Unknown error"
+ }`,
+ errorObj.stack ? `Stack Trace:\n${errorObj.stack}` : null,
+ ]
+ .filter(Boolean)
+ .join("\n");
+
+ return {
+ display: errorObj.stack || errorObj.message || String(error),
+ technical: technicalInfo,
+ hasTechnical: true,
+ };
+ }
+
+ // Fallback to original behavior
+ return {
+ display: errorObj.stack || errorObj.message || String(error),
+ technical: errorObj.stack || errorObj.message || String(error),
+ hasTechnical: Boolean(errorObj.stack),
+ };
}, [error]);
- const hasErrorDetails = type === "error" && errorMessage;
+ const hasErrorDetails = type === "error" && errorDetails.display;
+
+ // Copy technical details to clipboard
+ const copyErrorDetails = React.useCallback(async () => {
+ if (errorDetails.technical) {
+ try {
+ await navigator.clipboard.writeText(errorDetails.technical);
+ } catch (err) {
+ console.error("Failed to copy error details:", err);
+ // Fallback for older browsers
+ const textArea = document.createElement("textarea");
+ textArea.value = errorDetails.technical;
+ document.body.appendChild(textArea);
+ textArea.select();
+ document.execCommand("copy");
+ document.body.removeChild(textArea);
+ }
+ }
+ }, [errorDetails.technical]);
return (
-
-
- {type === "loading" ? (
-
- ) : (
- getToastIcon(type)
- )}
-
-
-
- {title && (
-
- {title}
-
- )}
+ {type === "loading" ? (
+
+ ) : (
+ getToastIcon(type)
+ )}
+
+
+ {title && (
- {message}
+ {title}
+ )}
+
+
+ {message}
+
- {/* Transaction Link */}
- {transactionId && (
+ {/* Transaction Link */}
+ {transactionId && (
+
View Transaction
- )}
+
+ )}
- {/* Error expand/collapse button */}
- {hasErrorDetails && (
+ {/* Error expand/collapse and copy buttons */}
+ {hasErrorDetails && (
+
setExpanded(!expanded)}
sx={{
display: "flex",
alignItems: "center",
- mt: 1,
- cursor: "pointer",
- fontSize: typography.fontSize.xs,
- color: colors.primary.main,
+ gap: spacing.sm,
+ flexWrap: "wrap",
}}
>
-
- {expanded ? "Hide Details" : "Show Details"}
-
- {expanded ? (
-
- ) : (
-
+ setExpanded(!expanded)}
+ sx={{
+ display: "flex",
+ alignItems: "center",
+ cursor: "pointer",
+ fontSize: typography.fontSize.xs,
+ color: colors.primary.main,
+ padding: "4px 8px",
+ borderRadius: borderRadius.sm,
+ background: `linear-gradient(135deg, rgba(249, 115, 22, 0.1) 0%, rgba(249, 115, 22, 0.05) 100%)`,
+ border: `1px solid rgba(249, 115, 22, 0.2)`,
+ transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)",
+ "&:hover": {
+ background: `linear-gradient(135deg, rgba(249, 115, 22, 0.2) 0%, rgba(249, 115, 22, 0.1) 100%)`,
+ border: `1px solid rgba(249, 115, 22, 0.4)`,
+ transform: "translateY(-1px)",
+ boxShadow: `0 4px 12px rgba(249, 115, 22, 0.2)`,
+ },
+ }}
+ >
+
+ {expanded ? "Hide Details" : "Show Details"}
+
+ {expanded ? (
+
+ ) : (
+
+ )}
+
+
+ {errorDetails.hasTechnical && (
+ }
+ onClick={copyErrorDetails}
+ sx={{
+ fontSize: typography.fontSize.xs,
+ color: colors.neutral[400],
+ textTransform: "none",
+ minWidth: "auto",
+ padding: "4px 8px",
+ borderRadius: borderRadius.sm,
+ background: `linear-gradient(135deg, rgba(115, 115, 115, 0.1) 0%, rgba(115, 115, 115, 0.05) 100%)`,
+ border: `1px solid rgba(115, 115, 115, 0.2)`,
+ transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)",
+ "&:hover": {
+ color: colors.neutral[200],
+ background: `linear-gradient(135deg, rgba(115, 115, 115, 0.2) 0%, rgba(115, 115, 115, 0.1) 100%)`,
+ border: `1px solid rgba(115, 115, 115, 0.4)`,
+ transform: "translateY(-1px)",
+ boxShadow: `0 4px 12px rgba(115, 115, 115, 0.2)`,
+ },
+ }}
+ >
+ Copy Details
+
)}
- )}
-
-
- {/* Only show close button for non-loading toasts */}
- {type !== "loading" && (
- onClose(id)}
- sx={{
- padding: "4px",
- color: colors.neutral[400],
- "&:hover": {
- color: colors.neutral[200],
- backgroundColor: "transparent",
- },
- }}
- >
-
-
+
)}
- {/* Collapsible Error Details */}
- {hasErrorDetails && (
-
-
- {errorMessage}
-
-
+ {/* Only show close button for non-loading toasts */}
+ {type !== "loading" && (
+ onClose(id)}
+ sx={{
+ padding: "6px",
+ color: colors.neutral[400],
+ borderRadius: "50%",
+ background: `linear-gradient(135deg, rgba(115, 115, 115, 0.1) 0%, rgba(115, 115, 115, 0.05) 100%)`,
+ border: `1px solid rgba(115, 115, 115, 0.2)`,
+ transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)",
+ "&:hover": {
+ color: colors.neutral[200],
+ background: `linear-gradient(135deg, rgba(115, 115, 115, 0.2) 0%, rgba(115, 115, 115, 0.1) 100%)`,
+ border: `1px solid rgba(115, 115, 115, 0.4)`,
+ boxShadow: `0 4px 12px rgba(115, 115, 115, 0.2)`,
+ },
+ }}
+ >
+
+
)}
-
+
+ {/* Collapsible Error Details */}
+ {hasErrorDetails && (
+
+
+ {errorDetails.display}
+
+
+ )}
+
);
};
diff --git a/packages/ui/src/Common/Toast/ToastContainer.tsx b/packages/ui/src/Common/Toast/ToastContainer.tsx
index b2a9a70c..04320aaa 100644
--- a/packages/ui/src/Common/Toast/ToastContainer.tsx
+++ b/packages/ui/src/Common/Toast/ToastContainer.tsx
@@ -1,8 +1,8 @@
import React, { useContext } from "react";
import { Box } from "@mui/material";
-import { AnimatePresence } from "framer-motion";
+import { AnimatePresence, motion } from "framer-motion";
import { Toast, ToastProps } from "./Toast";
-import { spacing } from "../../Theme/styleConstants";
+import { spacing, colors, borderRadius } from "../../Theme/styleConstants";
import { useToast } from "./useToast";
interface ToastContainerProps {
@@ -51,35 +51,76 @@ export const ToastContainer = ({
};
return (
-
-
- {toasts.map((toast) => (
-
- ))}
-
-
+
+
+ {toasts.map((toast, index) => (
+
+
+
+ ))}
+
+
+
);
};
diff --git a/packages/ui/src/Common/Toast/useToast.tsx b/packages/ui/src/Common/Toast/useToast.tsx
index 2dbd4d04..d849426a 100644
--- a/packages/ui/src/Common/Toast/useToast.tsx
+++ b/packages/ui/src/Common/Toast/useToast.tsx
@@ -2,6 +2,9 @@ import React, { useState, useCallback, useContext, createContext } from "react";
import { v4 as uuidv4 } from "uuid";
import { ToastProps, ToastType } from "./Toast";
+// Import enhanced error resolver
+import { resolveContractErrorEnhanced } from "@phoenix-protocol/utils/src/enhancedErrorResolver";
+
interface ToastContextType {
toasts: ToastProps[];
addToast: (options: Omit) => string;
@@ -112,38 +115,61 @@ export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({
: "Operation completed successfully!",
transactionId,
});
+
+ // Auto remove success toasts after 5 seconds
+ setTimeout(() => {
+ removeToast(toastId);
+ }, 5000);
+
return result;
} catch (error) {
- // Create a serializable error object
- const errorMessage =
+ // Enhanced error processing using the new error resolver
+ const errorString =
error instanceof Error
? error.message
: typeof error === "object" && error !== null && "message" in error
- ? error.message
- : "Operation failed";
+ ? String(error.message)
+ : String(error);
+ // Use enhanced error resolver to get user-friendly message
+ const errorResult = resolveContractErrorEnhanced(errorString);
+
+ // Create a serializable error object with enhanced details
const errorObj =
error instanceof Error
- ? { message: error.message, stack: error.stack }
+ ? {
+ message: error.message,
+ stack: error.stack,
+ userFriendlyMessage: errorResult.userFriendlyMessage,
+ errorCode: errorResult.errorCode,
+ contractType: errorResult.contractType,
+ }
: typeof error === "object" && error !== null
- ? error
- : { message: String(error) };
+ ? {
+ ...error,
+ userFriendlyMessage: errorResult.userFriendlyMessage,
+ errorCode: errorResult.errorCode,
+ contractType: errorResult.contractType,
+ }
+ : {
+ message: String(error),
+ userFriendlyMessage: errorResult.userFriendlyMessage,
+ errorCode: errorResult.errorCode,
+ contractType: errorResult.contractType,
+ };
updateToast(toastId, {
type: "error",
- message: errorMessage as string,
- error: errorObj as
- | string
- | Error
- | { message: string; stack?: string | undefined }
- | undefined, // Store a serializable error object
+ message: errorResult.userFriendlyMessage, // Use user-friendly message as primary
+ error: errorObj, // Store enhanced error details for "Show Details" functionality
});
- throw error;
- } finally {
- // Auto remove the toast after 5 seconds
+
+ // Auto remove error toasts after 10 seconds
setTimeout(() => {
removeToast(toastId);
- }, 5000);
+ }, 10000);
+
+ throw error;
}
},
[addToast, updateToast, removeToast]
diff --git a/packages/utils/jest.config.js b/packages/utils/jest.config.js
index b413e106..f1a69ee1 100644
--- a/packages/utils/jest.config.js
+++ b/packages/utils/jest.config.js
@@ -2,4 +2,6 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
+ testMatch: ['/src/**/__tests__/**/*.test.(ts|tsx|js)'],
+ roots: ['/src'],
};
\ No newline at end of file
diff --git a/packages/utils/package.json b/packages/utils/package.json
index 966fe8fd..0c8939c2 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -10,7 +10,7 @@
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
- "test": "echo \"Error: no test specified\" && exit 0"
+ "test": "jest"
},
"np": {
"publish": false,
diff --git a/packages/utils/src/__tests__/enhancedErrorResolver.test.ts b/packages/utils/src/__tests__/enhancedErrorResolver.test.ts
new file mode 100644
index 00000000..6017b828
--- /dev/null
+++ b/packages/utils/src/__tests__/enhancedErrorResolver.test.ts
@@ -0,0 +1,172 @@
+import {
+ resolveContractErrorEnhanced,
+ extractErrorCodeFromMessage,
+} from "../enhancedErrorResolver";
+
+describe("Enhanced Error Resolver", () => {
+ describe("extractErrorCodeFromMessage", () => {
+ it("should extract error codes from standard Soroban error format", () => {
+ const result = extractErrorCodeFromMessage("Error(Contract, #10)");
+ expect(result).toEqual({ code: 10 });
+ });
+
+ it("should extract error codes from HostError format", () => {
+ const result = extractErrorCodeFromMessage(
+ "HostError: Error(Contract, #300)"
+ );
+ expect(result).toEqual({ code: 300 });
+ });
+
+ it("should extract error codes from transaction simulation failed format", () => {
+ const result = extractErrorCodeFromMessage(
+ "Transaction simulation failed: HostError: Error(Contract, #101)"
+ );
+ expect(result).toEqual({ code: 101 });
+ });
+
+ it("should extract error codes from complex transaction simulation with event logs", () => {
+ const complexError =
+ 'Transaction simulation failed: "HostError: Error(Contract, #300) Event log (newest first): 0: [Diagnostic Event] contract:CCLZRD4E72T7JCZCN3P7KNPYNXFYKQCL64ECLX7WP5GNVYPHJGU2IO2G, topics:[error, Error(Contract, #300)], data:"escalating error to VM trap from failed host function call: call"';
+ const result = extractErrorCodeFromMessage(complexError);
+ expect(result).toEqual({ code: 300 });
+ });
+
+ it("should extract error codes from event log data", () => {
+ const eventLogError =
+ 'topics:[error, Error(Contract, #300)], data:["failing with contract error", 300]';
+ const result = extractErrorCodeFromMessage(eventLogError);
+ expect(result).toEqual({ code: 300 });
+ });
+
+ it("should return null for messages without error codes", () => {
+ const result = extractErrorCodeFromMessage("Generic error message");
+ expect(result).toBeNull();
+ });
+ });
+
+ describe("resolveContractErrorEnhanced", () => {
+ it("should resolve factory contract errors correctly", () => {
+ const result = resolveContractErrorEnhanced("Error(Contract, #100)");
+ expect(result.userFriendlyMessage).toBe(
+ "The selected token is not supported. Please choose a different token."
+ );
+ expect(result.errorCode).toBe(100);
+ });
+
+ it("should resolve pair contract errors correctly", () => {
+ const result = resolveContractErrorEnhanced("Error(Contract, #300)");
+ expect(result.userFriendlyMessage).toContain(
+ "price difference is too high"
+ );
+ expect(result.errorCode).toBe(300);
+ });
+
+ it("should resolve stake contract errors correctly", () => {
+ const result = resolveContractErrorEnhanced("Error(Contract, #500)");
+ expect(result.userFriendlyMessage).toBe(
+ "The staking system is not ready yet. Please try again later."
+ );
+ expect(result.errorCode).toBe(500);
+ });
+
+ it("should resolve multihop contract errors correctly", () => {
+ const result = resolveContractErrorEnhanced("Error(Contract, #200)");
+ expect(result.userFriendlyMessage).toBe(
+ "The multi-step trading system is not ready yet. Please try again later."
+ );
+ expect(result.errorCode).toBe(200);
+ });
+
+ it("should resolve vesting contract errors correctly", () => {
+ const result = resolveContractErrorEnhanced("Error(Contract, #700)");
+ expect(result.userFriendlyMessage).toBe(
+ "The token vesting system is not ready yet. Please try again later."
+ );
+ expect(result.errorCode).toBe(700);
+ });
+
+ it("should handle unknown error codes gracefully", () => {
+ const result = resolveContractErrorEnhanced("Error(Contract, #999)");
+ expect(result.userFriendlyMessage).toContain(
+ "Something went wrong (error code: 999)"
+ );
+ expect(result.errorCode).toBe(999);
+ });
+
+ it("should handle messages without error codes", () => {
+ const result = resolveContractErrorEnhanced("Generic network error");
+ expect(result.userFriendlyMessage).toContain(
+ "Something unexpected happened"
+ );
+ expect(result.errorCode).toBeNull();
+ });
+
+ it("should infer contract type from error code ranges", () => {
+ const result = resolveContractErrorEnhanced("Error(Contract, #300)");
+ expect(result.contractType).toBe("pair");
+ expect(result.userFriendlyMessage).toContain(
+ "price difference is too high"
+ );
+ });
+
+ it("should infer contract type from context when error code is unknown", () => {
+ const result = resolveContractErrorEnhanced(
+ "Liquidity pool error: Error(Contract, #999)"
+ );
+ expect(result.contractType).toBe("pair");
+ expect(result.userFriendlyMessage).toContain(
+ "Something went wrong (error code: 999)"
+ );
+ });
+
+ it("should preserve technical details", () => {
+ const errorMessage =
+ "Transaction simulation failed: HostError: Error(Contract, #101)";
+ const result = resolveContractErrorEnhanced(errorMessage);
+ expect(result.technicalDetails).toBe(errorMessage);
+ expect(result.userFriendlyMessage).toBe(
+ "This token pair already exists. You can find it in the pools section."
+ );
+ });
+
+ it("should handle real-world complex transaction simulation errors", () => {
+ const complexRealError =
+ 'Transaction simulation failed: "HostError: Error(Contract, #300) Event log (newest first): 0: [Diagnostic Event] contract:CCLZRD4E72T7JCZCN3P7KNPYNXFYKQCL64ECLX7WP5GNVYPHJGU2IO2G, topics:[error, Error(Contract, #300)], data:"escalating error to VM trap from failed host function call: call" 1: [Diagnostic Event] contract:CCLZRD4E72T7JCZCN3P7KNPYNXFYKQCL64ECLX7WP5GNVYPHJGU2IO2G, topics:[error, Error(Contract, #300)], data:["contract call failed", swap, [GAPRPZYCIV3QPMCTWSRDNY64EJMZNCJFUCTJHQDQNW6RJ66TEVEH5UDU, CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA, 60000000000, Void, 100, Void, Void]]';
+ const result = resolveContractErrorEnhanced(complexRealError);
+ expect(result.errorCode).toBe(300);
+ expect(result.userFriendlyMessage).toContain(
+ "price difference is too high"
+ );
+ expect(result.contractType).toBe("pair");
+ expect(result.technicalDetails).toBe(complexRealError);
+ });
+ });
+});
+
+// Example usage demonstrations
+console.log("=== Enhanced Error Resolver Examples ===");
+
+// Example 1: Standard contract error
+const error1 = resolveContractErrorEnhanced("Error(Contract, #300)");
+console.log("Example 1 - Standard error:");
+console.log("User message:", error1.userFriendlyMessage);
+console.log("Error code:", error1.errorCode);
+console.log("Technical:", error1.technicalDetails);
+console.log("");
+
+// Example 2: Complex transaction error
+const error2 = resolveContractErrorEnhanced(
+ "Transaction simulation failed: HostError: Error(Contract, #101)"
+);
+console.log("Example 2 - Transaction simulation error:");
+console.log("User message:", error2.userFriendlyMessage);
+console.log("Error code:", error2.errorCode);
+console.log("Technical:", error2.technicalDetails);
+console.log("");
+
+// Example 3: Unknown error
+const error3 = resolveContractErrorEnhanced("Network connection failed");
+console.log("Example 3 - Unknown error:");
+console.log("User message:", error3.userFriendlyMessage);
+console.log("Error code:", error3.errorCode);
+console.log("Technical:", error3.technicalDetails);
diff --git a/packages/utils/src/enhancedErrorResolver.ts b/packages/utils/src/enhancedErrorResolver.ts
new file mode 100644
index 00000000..b3aa7184
--- /dev/null
+++ b/packages/utils/src/enhancedErrorResolver.ts
@@ -0,0 +1,294 @@
+// Enhanced error code extraction that supports multiple error formats
+export function extractErrorCodeFromMessage(
+ message: string
+): { code: number; contractType?: string } | null {
+ // Pattern 1: Standard Soroban error - "Error(Contract, #10)"
+ const standardMatch = message.match(/Error\(Contract,\s*#(\d+)\)/);
+ if (standardMatch) {
+ return { code: parseInt(standardMatch[1], 10) };
+ }
+
+ // Pattern 2: Detailed error messages - "HostError: Error(Contract, #10)"
+ const hostErrorMatch = message.match(
+ /HostError:\s*Error\(Contract,\s*#(\d+)\)/
+ );
+ if (hostErrorMatch) {
+ return { code: parseInt(hostErrorMatch[1], 10) };
+ }
+
+ // Pattern 3: Transaction simulation failed with diagnostic events
+ // Example: "Transaction simulation failed: "HostError: Error(Contract, #300) Event log..."
+ const simulationDetailedMatch = message.match(
+ /Transaction simulation failed:\s*"?HostError:\s*Error\(Contract,\s*#(\d+)\)/i
+ );
+ if (simulationDetailedMatch) {
+ return { code: parseInt(simulationDetailedMatch[1], 10) };
+ }
+
+ // Pattern 4: Error within event logs - look for error codes in event data
+ const eventLogMatch = message.match(
+ /topics:\[error,\s*Error\(Contract,\s*#(\d+)\)\]|data:.*?Error\(Contract,\s*#(\d+)\)|failing with contract error.*?(\d+)/i
+ );
+ if (eventLogMatch) {
+ const errorCode = eventLogMatch[1] || eventLogMatch[2] || eventLogMatch[3];
+ return { code: parseInt(errorCode, 10) };
+ }
+
+ // Pattern 5: Contract-specific error with contract name
+ const contractSpecificMatch = message.match(
+ /(\w+)(?:Contract)?.*?Error.*?#(\d+)/i
+ );
+ if (contractSpecificMatch) {
+ return {
+ code: parseInt(contractSpecificMatch[2], 10),
+ contractType: contractSpecificMatch[1].toLowerCase(),
+ };
+ }
+
+ // Pattern 6: Simple transaction simulation failed format
+ const simulationMatch = message.match(
+ /simulation failed.*?Error\(Contract,\s*#(\d+)\)/i
+ );
+ if (simulationMatch) {
+ return { code: parseInt(simulationMatch[1], 10) };
+ }
+
+ return null;
+}
+
+// Contract error definitions organized by error code ranges
+// This approach works better since multihop can throw pair errors, factory errors, etc.
+// Note: Soroban token standard (SEP-41) uses panic-based error handling rather than numbered codes
+const ERROR_CODE_DEFINITIONS: { [code: number]: string } = {
+ // Note: Error codes 1-10 are reserved but not officially defined by SEP-41
+ // The Soroban token standard uses panic! with descriptive messages instead of numbered codes
+ // These are generic fallbacks for custom token implementations that might use numbered errors
+ 1: "There was an issue with your token transaction. Please check your balance and try again.",
+ 2: "You don't have permission to perform this action with these tokens.",
+ 3: "This token operation is currently restricted. Please try again later.",
+ 4: "The token amount you entered is not valid. Please enter a positive number.",
+ 5: "The token operation failed. Please check your inputs and try again.",
+ 6: "You've exceeded the spending allowance for these tokens.",
+ 7: "The requested token operation could not be completed.",
+ 8: "There was an issue with the token transaction. Please verify the details and try again.",
+ 9: "The token operation could not be processed. Please try again.",
+ 10: "You don't have enough tokens to complete this request. If you are using XLM, make sure you have enough XLM for reserves and trustlines.",
+
+ // Phoenix Factory Contract Errors (100-112)
+ 100: "The selected token is not supported. Please choose a different token.",
+ 101: "This token pair already exists. You can find it in the pools section.",
+ 102: "You cannot create a pair with the same token twice. Please select two different tokens.",
+ 103: "The system is already set up and ready to use.",
+ 104: "The system is not ready yet. Please try again later.",
+ 105: "No administrator has been assigned to manage this system.",
+ 106: "You don't have permission to perform this action. Only administrators can do this.",
+ 107: "The new administrator is the same as the current one. No changes needed.",
+ 108: "This token pair doesn't exist yet. You may need to create it first.",
+ 109: "Too many accounts have been whitelisted. The limit has been reached.",
+ 110: "Your account is not approved for this action. Please contact support.",
+ 111: "The settings for this token pair haven't been configured yet.",
+ 112: "The settings for this token pair have already been set up.",
+
+ // Phoenix Multihop Contract Errors (200-206)
+ 200: "The multi-step trading system is not ready yet. Please try again later.",
+ 201: "You need to specify a valid trading path. The path cannot be empty.",
+ 202: "You don't have enough tokens to complete this multi-step trade.",
+ 203: "The price moved too much during your multi-step trade. Try with higher slippage tolerance.",
+ 204: "One or more trading pools in your path don't have enough tokens available.",
+ 205: "There's an invalid token in your trading path. Please check your route.",
+ 206: "Your multi-step trade took too long and expired. Please try again.",
+
+ // Phoenix Pair Contract Errors (300-332)
+ 300: "The price difference is too high for your safety. Try swapping a smaller amount or increase your slippage tolerance in settings.",
+ 301: "This trading pair is not ready yet. Please try again later.",
+ 302: "This trading pair is already set up and working.",
+ 303: "The slippage tolerance you set is too high for providing liquidity. Please lower it for your safety.",
+ 304: "You need to enter an amount greater than zero for at least one token to provide liquidity.",
+ 305: "The minimum amount you'll receive doesn't meet your requirements. Try adjusting your settings.",
+ 306: "Both pools and deposit amounts must be positive numbers to split your deposit.",
+ 307: "The total fees cannot be more than 100%. Please check the fee settings.",
+ 308: "The minimum amount of the first token is higher than what you want to provide.",
+ 309: "The minimum amount of the second token is higher than what you want to provide.",
+ 310: "You're trying to provide more of the first token than you intended.",
+ 311: "You're providing less of the first token than the minimum required.",
+ 312: "You're trying to provide more of the second token than you intended.",
+ 313: "You're providing less of the second token than the minimum required.",
+ 314: "The total shares amount cannot be zero. Please check your liquidity amounts.",
+ 315: "The amounts you want to provide must be greater than zero.",
+ 316: "The minimum amounts cannot be negative numbers.",
+ 317: "This pool doesn't have enough tokens available for your trade. Try a smaller amount.",
+ 318: "You don't have enough tokens in your wallet for this swap.",
+ 319: "This trading pair is currently inactive. Please try again later.",
+ 320: "You don't have permission to perform this action.",
+ 321: "There's something wrong with your swap settings. Please check and try again.",
+ 322: "This liquidity pool is empty. No trades can be made right now.",
+ 323: "You cannot provide liquidity with zero amounts. Please enter valid amounts.",
+ 324: "You're trying to withdraw more than your available shares allow.",
+ 325: "This trade would affect the price too much. Try a smaller amount.",
+ 326: "Your transaction took too long and expired. Please try again.",
+ 327: "The fee settings are incorrect. Please contact support.",
+ 328: "The ratio between tokens is outside acceptable limits.",
+ 329: "The pool has run out of tokens. Please try again later.",
+ 330: "The price moved too much during your transaction. Try again with higher slippage tolerance.",
+ 331: "You would receive less tokens than expected. Try a smaller trade or higher slippage.",
+ 332: "This swap would result in a negative balance, which is not allowed.",
+
+ // Phoenix Stake Contract Errors (500-520)
+ 500: "The staking system is not ready yet. Please try again later.",
+ 501: "The staking system is already set up and working.",
+ 502: "You don't have permission to perform this staking action.",
+ 503: "You don't have enough staked tokens for this action.",
+ 504: "You need to stake more than zero tokens. Please enter a valid amount.",
+ 505: "Your staking period hasn't finished yet. Please wait before withdrawing.",
+ 506: "You don't have any rewards to claim right now.",
+ 507: "The staking pool is full. Please try again later or stake in a different pool.",
+ 508: "The amount you want to stake is below the minimum required. Please stake more.",
+ 509: "The amount you want to stake is above the maximum allowed. Please stake less.",
+ 510: "Staking is temporarily paused. Please try again later.",
+ 511: "There's an issue with the staking settings. Please contact support.",
+ 512: "There was an error calculating your rewards. Please try again.",
+ 513: "You need to wait longer before you can unstake your tokens.",
+ 514: "You don't have any active stakes in this pool.",
+ 515: "The stake you're looking for doesn't exist.",
+ 516: "You've already withdrawn from this stake.",
+ 517: "Withdrawing early will result in a penalty fee.",
+ 518: "The reward pool has run out of tokens. Please contact support.",
+ 519: "The reward rate settings are incorrect. Please contact support.",
+ 520: "You need to wait before you can stake again.",
+
+ // Phoenix Vesting Contract Errors (700-721)
+ 700: "The token vesting system is not ready yet. Please try again later.",
+ 701: "The token vesting system is already set up and working.",
+ 702: "You don't have access to this vesting schedule. Please check with the administrator.",
+ 703: "The vesting schedule you're looking for doesn't exist.",
+ 704: "You don't have any tokens available to withdraw right now.",
+ 705: "Your vesting period hasn't started yet. Please wait until the start date.",
+ 706: "There's an issue with the vesting settings. Please contact support.",
+ 707: "Your cliff period hasn't ended yet. You need to wait longer before claiming tokens.",
+ 708: "You're trying to claim more tokens than are available in your vesting schedule.",
+ 709: "You cannot create a vesting schedule with zero tokens.",
+ 710: "The vesting period must be longer than zero days.",
+ 711: "The cliff period cannot be longer than the total vesting period.",
+ 712: "The beneficiary address is not valid. Please check the address.",
+ 713: "You've already claimed all tokens from this vesting schedule.",
+ 714: "There aren't enough tokens in the contract to fulfill your vesting schedule.",
+ 715: "The vesting start time cannot be in the past.",
+ 716: "Too many vesting schedules have been created. The limit has been reached.",
+ 717: "This vesting schedule has been temporarily paused.",
+ 718: "Only the administrator can change vesting settings.",
+ 719: "Your vesting tokens are currently locked and cannot be accessed.",
+ 720: "Early withdrawal is not allowed for this vesting schedule.",
+ 721: "There was an error calculating your vesting amounts. Please contact support.",
+};
+
+/**
+ * Attempts to determine the contract type based on error code ranges or context
+ */
+function inferContractType(
+ errorCode: number,
+ errorMessage: string
+): string | null {
+ // Determine contract type based on error code ranges
+ if (errorCode >= 1 && errorCode <= 10) {
+ // Note: These are generic token errors since SEP-41 doesn't define standard numbered codes
+ return "token";
+ } else if (errorCode >= 100 && errorCode <= 112) {
+ return "factory";
+ } else if (errorCode >= 200 && errorCode <= 206) {
+ return "multihop";
+ } else if (errorCode >= 300 && errorCode <= 332) {
+ return "pair";
+ } else if (errorCode >= 500 && errorCode <= 520) {
+ return "stake";
+ } else if (errorCode >= 700 && errorCode <= 721) {
+ return "vesting";
+ }
+
+ // Fallback: try to infer from error message context
+ const lowerMessage = errorMessage.toLowerCase();
+ if (
+ lowerMessage.includes("factory") ||
+ lowerMessage.includes("pair creation")
+ ) {
+ return "factory";
+ }
+ if (
+ lowerMessage.includes("liquidity") ||
+ lowerMessage.includes("swap") ||
+ lowerMessage.includes("pool")
+ ) {
+ return "pair";
+ }
+ if (lowerMessage.includes("stake") || lowerMessage.includes("staking")) {
+ return "stake";
+ }
+ if (lowerMessage.includes("multihop") || lowerMessage.includes("path")) {
+ return "multihop";
+ }
+ if (lowerMessage.includes("vesting") || lowerMessage.includes("cliff")) {
+ return "vesting";
+ }
+ if (
+ lowerMessage.includes("token") ||
+ lowerMessage.includes("balance") ||
+ lowerMessage.includes("transfer")
+ ) {
+ return "token";
+ }
+
+ return null;
+}
+
+/**
+ * Enhanced contract error resolver that provides user-friendly error messages
+ */
+export function resolveContractErrorEnhanced(
+ errorMessage: string,
+ contractAddress?: string,
+ contractType?: string
+): {
+ userFriendlyMessage: string;
+ errorCode: number | null;
+ contractType: string | null;
+ technicalDetails: string;
+} {
+ const extractedError = extractErrorCodeFromMessage(errorMessage);
+
+ if (!extractedError) {
+ return {
+ userFriendlyMessage:
+ "Something unexpected happened. Please try again, and if the problem continues, contact our support team.",
+ errorCode: null,
+ contractType: null,
+ technicalDetails: errorMessage,
+ };
+ }
+
+ const { code } = extractedError;
+
+ // Determine contract type based on error code or context
+ const finalContractType =
+ contractType ||
+ extractedError.contractType ||
+ inferContractType(code, errorMessage);
+
+ // Get user-friendly message directly from error code
+ const userFriendlyMessage =
+ ERROR_CODE_DEFINITIONS[code] ||
+ `Something went wrong (error code: ${code}). Please try again or contact our support team with this error code.`;
+
+ return {
+ userFriendlyMessage,
+ errorCode: code,
+ contractType: finalContractType,
+ technicalDetails: errorMessage,
+ };
+}
+
+/**
+ * Legacy function for backward compatibility
+ */
+export function resolveContractErrorLegacy(eventString: string): string {
+ const result = resolveContractErrorEnhanced(eventString);
+ return result.userFriendlyMessage;
+}
diff --git a/packages/utils/src/errorResolver.ts b/packages/utils/src/errorResolver.ts
index 4a7d3e62..53a73406 100644
--- a/packages/utils/src/errorResolver.ts
+++ b/packages/utils/src/errorResolver.ts
@@ -1,13 +1,11 @@
+// Legacy error definitions for backward compatibility
enum ContractError {
SpreadExceedsLimit = 1,
-
ProvideLiquiditySlippageToleranceTooHigh = 2,
ProvideLiquidityAtLeastOneTokenMustBeBiggerThenZero = 3,
-
WithdrawLiquidityMinimumAmountOfAOrBIsNotSatisfied = 4,
SplitDepositBothPoolsAndDepositMustBePositive = 5,
ValidateFeeBpsTotalFeesCantBeGreaterThen100 = 6,
-
GetDepositAmountsMinABiggerThenDesiredA = 7,
GetDepositAmountsMinBBiggerThenDesiredB = 8,
GetDepositAmountsAmountABiggerThenDesiredA = 9,
@@ -19,23 +17,39 @@ enum ContractError {
MinAmountsBelowZero = 15,
}
+// Import enhanced error resolver
+import {
+ resolveContractErrorEnhanced as resolveContractErrorEnhanced,
+ extractErrorCodeFromMessage,
+} from "./enhancedErrorResolver";
+
/**
- * Extracts the error code from a diagnostic event string.
- * @param {string} eventString
- * @returns {number | null} The error code or null if not found
- * @example
- * extractErrorCodeFromDiagnosticEvent("Error(Contract, #1)") // 1
- * extractErrorCodeFromDiagnosticEvent("Error(Contract, #2)") // 2
+ * Legacy function - extracts the error code from a diagnostic event string.
+ * @deprecated Use extractErrorCodeFromMessage from enhancedErrorResolver instead
*/
function extractErrorCodeFromDiagnosticEvent(
eventString: string
): number | null {
- const errorRegex = /Error\(Contract, #(\d+)\)/;
- const match = eventString.match(errorRegex);
- return match ? parseInt(match[1], 10) : null;
+ return extractErrorCodeFromMessage(eventString)?.code || null;
}
+/**
+ * Enhanced contract error resolver that provides user-friendly error messages
+ * with fallback to legacy error handling for backward compatibility
+ */
export function resolveContractError(eventString: string): string {
+ // Try enhanced resolver first
+ const enhancedResult = resolveContractErrorEnhanced(eventString);
+
+ // If enhanced resolver found a meaningful message, use it
+ if (
+ enhancedResult.errorCode !== null &&
+ enhancedResult.userFriendlyMessage !== "Unknown error occurred."
+ ) {
+ return enhancedResult.userFriendlyMessage;
+ }
+
+ // Fallback to legacy resolver for backward compatibility
const errorCode = extractErrorCodeFromDiagnosticEvent(eventString);
switch (errorCode) {
@@ -70,6 +84,9 @@ export function resolveContractError(eventString: string): string {
case ContractError.MinAmountsBelowZero:
return "The minimum amounts cannot be below zero.";
default:
- return "Unknown error.";
+ return enhancedResult.userFriendlyMessage || "Unknown error.";
}
}
+
+// Re-export for convenience (but use the enhanced resolver internally)
+export { extractErrorCodeFromMessage };
diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index b8ac1801..7a34b6de 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -9,6 +9,7 @@ export * from "./sep10";
export * from "./graphql";
export * from "./prices";
export * from "./errorResolver";
+export * from "./enhancedErrorResolver";
export * from "./trustlines";
export * from "./api";
export * as TradeAPi from "./trade_api";