|
| 1 | +/** |
| 2 | + * error: Stellar Error Handling & User Messaging |
| 3 | + * Centralized error handler that translates Stellar/Soroban errors into user-friendly messages. |
| 4 | + */ |
| 5 | + |
| 6 | +export const StellarErrorType = { |
| 7 | + INVALID_AMOUNT: "InvalidAmount", |
| 8 | + INSUFFICIENT_FUNDING: "InsufficientFunding", |
| 9 | + UNAUTHORIZED: "Unauthorized", |
| 10 | + DEADLINE_NOT_REACHED: "DeadlineNotReached", |
| 11 | + SHARE_SUM_EXCEEDS_RENT: "ShareSumExceedsRent", |
| 12 | + NOTHING_TO_REFUND: "NothingToRefund", |
| 13 | + RPC_NETWORK_TIMEOUT: "RpcNetworkTimeout", |
| 14 | + RPC_NODE_UNAVAILABLE: "RpcNodeUnavailable", |
| 15 | + RPC_SIMULATION_FAILURE: "RpcSimulationFailure", |
| 16 | + FREIGHTER_REJECTED: "FreighterRejected", |
| 17 | + FREIGHTER_LOCKED: "FreighterLocked", |
| 18 | + FREIGHTER_NOT_INSTALLED: "FreighterNotInstalled", |
| 19 | + UNKNOWN: "Unknown", |
| 20 | +} as const; |
| 21 | + |
| 22 | +export type StellarErrorType = (typeof StellarErrorType)[keyof typeof StellarErrorType]; |
| 23 | + |
| 24 | +export interface UserFriendlyError { |
| 25 | + type: StellarErrorType; |
| 26 | + message: string; |
| 27 | + guidance: string; |
| 28 | +} |
| 29 | + |
| 30 | +const ERROR_MAPPINGS: Record<number | string, UserFriendlyError> = { |
| 31 | + // Soroban Contract Errors (matched by code or name) |
| 32 | + 1: { |
| 33 | + type: StellarErrorType.INVALID_AMOUNT, |
| 34 | + message: "The provided amount is invalid.", |
| 35 | + guidance: "Please check the amount and try again. It must be a positive number and meet the minimum rent requirement.", |
| 36 | + }, |
| 37 | + 2: { |
| 38 | + type: StellarErrorType.INSUFFICIENT_FUNDING, |
| 39 | + message: "Insufficient funding in the escrow.", |
| 40 | + guidance: "The contract does not have enough tokens to complete this release. Ensure all roommates have contributed.", |
| 41 | + }, |
| 42 | + 3: { |
| 43 | + type: StellarErrorType.UNAUTHORIZED, |
| 44 | + message: "You are not authorized to perform this action.", |
| 45 | + guidance: "Ensure you are using the correct account (e.g., the landlord or a registered roommate).", |
| 46 | + }, |
| 47 | + 4: { |
| 48 | + type: StellarErrorType.DEADLINE_NOT_REACHED, |
| 49 | + message: "The deadline has not been reached yet.", |
| 50 | + guidance: "Refunds or certain actions are only available after the escrow deadline has passed.", |
| 51 | + }, |
| 52 | + 5: { |
| 53 | + type: StellarErrorType.SHARE_SUM_EXCEEDS_RENT, |
| 54 | + message: "Total roommate shares exceed the rent amount.", |
| 55 | + guidance: "The sum of all roommate obligations cannot be greater than the total rent. Please adjust the shares.", |
| 56 | + }, |
| 57 | + 6: { |
| 58 | + type: StellarErrorType.NOTHING_TO_REFUND, |
| 59 | + message: "No funds available to refund.", |
| 60 | + guidance: "This account has no contributed balance in the escrow to reclaim.", |
| 61 | + }, |
| 62 | + |
| 63 | + // RPC Errors |
| 64 | + "TIMEOUT": { |
| 65 | + type: StellarErrorType.RPC_NETWORK_TIMEOUT, |
| 66 | + message: "Network request timed out.", |
| 67 | + guidance: "The Stellar network is taking too long to respond. Please check your internet connection or try again in a few moments.", |
| 68 | + }, |
| 69 | + "NODE_UNAVAILABLE": { |
| 70 | + type: StellarErrorType.RPC_NODE_UNAVAILABLE, |
| 71 | + message: "The Soroban RPC node is currently unavailable.", |
| 72 | + guidance: "We're having trouble connecting to the Stellar network. Please try again later.", |
| 73 | + }, |
| 74 | + "SIMULATION_FAILURE": { |
| 75 | + type: StellarErrorType.RPC_SIMULATION_FAILURE, |
| 76 | + message: "Transaction simulation failed.", |
| 77 | + guidance: "The transaction would fail if submitted. This often happens if contract conditions aren't met.", |
| 78 | + }, |
| 79 | + |
| 80 | + // Freighter Errors |
| 81 | + "User declined": { |
| 82 | + type: StellarErrorType.FREIGHTER_REJECTED, |
| 83 | + message: "Transaction rejected in Freighter.", |
| 84 | + guidance: "You cancelled the transaction in your wallet. If this was a mistake, please try again and approve the request.", |
| 85 | + }, |
| 86 | + "Wallet is locked": { |
| 87 | + type: StellarErrorType.FREIGHTER_LOCKED, |
| 88 | + message: "Freighter wallet is locked.", |
| 89 | + guidance: "Please unlock your Freighter extension and try again.", |
| 90 | + }, |
| 91 | + "Freighter not found": { |
| 92 | + type: StellarErrorType.FREIGHTER_NOT_INSTALLED, |
| 93 | + message: "Freighter extension not found.", |
| 94 | + guidance: "Please install the Freighter wallet extension to interact with this application.", |
| 95 | + }, |
| 96 | +}; |
| 97 | + |
| 98 | +/** |
| 99 | + * Translates a raw error from Stellar, Soroban, or Freighter into a user-friendly message. |
| 100 | + * @param error The raw error object or message string. |
| 101 | + */ |
| 102 | +export function translateStellarError(error: any): UserFriendlyError { |
| 103 | + let errorKey: string | number = "UNKNOWN"; |
| 104 | + |
| 105 | + if (typeof error === "string") { |
| 106 | + const msg = error.toLowerCase(); |
| 107 | + if (msg.includes("declined") || msg.includes("reject")) errorKey = "User declined"; |
| 108 | + else if (msg.includes("timeout")) errorKey = "TIMEOUT"; |
| 109 | + else if (msg.includes("unavailable") || msg.includes("fetch")) errorKey = "NODE_UNAVAILABLE"; |
| 110 | + else if (msg.includes("lock")) errorKey = "Wallet is locked"; |
| 111 | + else errorKey = error; |
| 112 | + } else if (error && typeof error === "object") { |
| 113 | + // Handle Soroban contract error codes (e.g., from simulation or resultXdr) |
| 114 | + if (typeof error.code === "number") { |
| 115 | + errorKey = error.code; |
| 116 | + } else if (error.message) { |
| 117 | + // Handle known error message substrings |
| 118 | + const msg = error.message.toLowerCase(); |
| 119 | + if (msg.includes("timeout")) errorKey = "TIMEOUT"; |
| 120 | + else if (msg.includes("unavailable") || msg.includes("fetch")) errorKey = "NODE_UNAVAILABLE"; |
| 121 | + else if (msg.includes("declined") || msg.includes("reject")) errorKey = "User declined"; |
| 122 | + else if (msg.includes("lock")) errorKey = "Wallet is locked"; |
| 123 | + else if (msg.includes("simulation")) errorKey = "SIMULATION_FAILURE"; |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + const mapping = ERROR_MAPPINGS[errorKey]; |
| 128 | + if (mapping) { |
| 129 | + return mapping; |
| 130 | + } |
| 131 | + |
| 132 | + // Fallback for unmapped errors |
| 133 | + return { |
| 134 | + type: StellarErrorType.UNKNOWN, |
| 135 | + message: "An unexpected Stellar error occurred.", |
| 136 | + guidance: typeof error?.message === "string" ? error.message : "If the problem persists, please contact support with details of the action you were performing.", |
| 137 | + }; |
| 138 | +} |
0 commit comments