From 7e3fbfd95e89607480783bd16a06efcebca1d72f Mon Sep 17 00:00:00 2001 From: BountySpaghetti Date: Sun, 26 Jul 2026 02:11:09 +0100 Subject: [PATCH] fix(wallet): handle missing wallet, wrong network, trustline, balance, and user rejection - Add lib/stellar/stellarErrors.js: reusable error mapping utility - StellarProvider: wallet detection, network mismatch check, validateForPayment() - PaymentModal: no-wallet install prompts, network mismatch warning, trustline/balance checks - WalletConnectButton: install prompts when no extension, network mismatch badge - useStellarPayment: graceful user rejection handling --- components/stellar/PaymentModal.jsx | 304 ++++++++++++++++----- components/stellar/StellarProvider.jsx | 179 ++++++++++-- components/stellar/WalletConnectButton.jsx | 87 +++++- hooks/useStellarPayment.js | 64 ++--- lib/stellar/stellarErrors.js | 181 ++++++++++++ 5 files changed, 683 insertions(+), 132 deletions(-) create mode 100644 lib/stellar/stellarErrors.js diff --git a/components/stellar/PaymentModal.jsx b/components/stellar/PaymentModal.jsx index 557b3a3..3738dbc 100644 --- a/components/stellar/PaymentModal.jsx +++ b/components/stellar/PaymentModal.jsx @@ -17,10 +17,17 @@ import { ExternalLink, ArrowRight, QrCode, + ShieldAlert, + WifiOff, } from "lucide-react"; import { useStellar } from "./StellarProvider"; import useStellarPayment from "@/hooks/useStellarPayment"; import Sep7QrCode from "./Sep7QrCode"; +import { + mapStellarError, + isUserRejection, + WALLET_INSTALL_LINKS, +} from "@/lib/stellar/stellarErrors"; export default function PaymentModal({ isOpen, @@ -29,30 +36,52 @@ export default function PaymentModal({ itemType, onSuccess, }) { - const { connectedWallet, walletInfo, connectWallet, network, isConnecting } = - useStellar(); + const { + connectedWallet, + walletInfo, + connectWallet, + network, + isConnecting, + hasWalletExtension, + networkMismatch, + validateForPayment, + } = useStellar(); const { initializePayment, executePayment, isProcessing } = useStellarPayment(); - const [step, setStep] = useState("preview"); // preview | confirm | processing | success | error + const [step, setStep] = useState("preview"); const [paymentData, setPaymentData] = useState(null); const [result, setResult] = useState(null); const [error, setError] = useState(null); + const [errorDetail, setErrorDetail] = useState(null); const [showQr, setShowQr] = useState(false); + const [preCheckIssues, setPreCheckIssues] = useState([]); - // Reset state when modal opens/closes useEffect(() => { if (isOpen) { setStep("preview"); setPaymentData(null); setResult(null); setError(null); + setErrorDetail(null); setShowQr(false); + setPreCheckIssues([]); } }, [isOpen]); + // Run pre-payment validation when modal opens + useEffect(() => { + if (isOpen && connectedWallet) { + validateForPayment(item?.price).then((issues) => { + setPreCheckIssues(issues); + }); + } + }, [isOpen, connectedWallet, item?.price, validateForPayment]); + const handleInitiate = async () => { setStep("processing"); + setError(null); + setErrorDetail(null); const data = await initializePayment({ itemType, itemId: item._id, @@ -68,15 +97,34 @@ export default function PaymentModal({ const handleConfirm = async () => { setStep("processing"); - const success = await executePayment(paymentData); + setError(null); + setErrorDetail(null); - if (success) { - setResult(success); - setStep("success"); - onSuccess?.(success); - } else { + try { + const success = await executePayment(paymentData); + + if (success) { + setResult(success); + setStep("success"); + onSuccess?.(success); + } else { + setStep("error"); + setError("Transaction failed. Please try again."); + } + } catch (err) { + const mapped = mapStellarError(err); + if (mapped) { + setError(mapped.message); + setErrorDetail(mapped); + } else if (isUserRejection(err)) { + setStep("preview"); + setError(null); + setErrorDetail(null); + return; + } else { + setError(err.message || "Transaction failed. Please try again."); + } setStep("error"); - setError("Transaction failed. Please try again."); } }; @@ -85,7 +133,9 @@ export default function PaymentModal({ setPaymentData(null); setResult(null); setError(null); + setErrorDetail(null); setShowQr(false); + setPreCheckIssues([]); onClose(); }; @@ -100,6 +150,10 @@ export default function PaymentModal({ connectedWallet && parseFloat(walletInfo?.usdcBalance || 0) < (item?.price || 0); + const hasBlockingIssues = preCheckIssues.some( + (i) => i.type === "no_wallet" || i.type === "network" || i.type === "trustline" + ); + return ( @@ -132,8 +186,55 @@ export default function PaymentModal({ - {/* Wallet Not Connected */} - {!connectedWallet && step === "preview" && ( + {/* No Wallet Extension Installed */} + {!connectedWallet && step === "preview" && !hasWalletExtension && ( +
+
+ +
+

+ No Stellar wallet detected +

+

+ Install a Stellar wallet extension to make purchases on + DeenBridge. +

+
+
+
+ + Install Freighter + + + + Install xBull + + + + Use Albedo (web wallet) + + +
+
+ )} + + {/* Wallet Not Connected (extension exists) */} + {!connectedWallet && step === "preview" && hasWalletExtension && (

Connect your Stellar wallet to continue @@ -158,41 +259,96 @@ export default function PaymentModal({

)} - {/* Wallet Connected - Preview */} - {connectedWallet && step === "preview" && ( -
-
- - Your Wallet - - - {truncateAddress(connectedWallet)} - -
-
- - USDC Balance - - - {parseFloat(walletInfo?.usdcBalance || 0).toFixed(2)} USDC - -
-
- Network - {network} + {/* Network Mismatch Warning */} + {connectedWallet && networkMismatch && step === "preview" && ( +
+
+ +
+

+ Wrong Network +

+

+ Your wallet is on a different network. Please switch to{" "} + {network} in your wallet settings and try + again. +

+
- {hasInsufficientBalance && ( -

- Insufficient USDC balance. Please add funds to your wallet. -

- )}
)} + {/* Pre-check Issues (trustline, balance) */} + {connectedWallet && + !networkMismatch && + step === "preview" && + preCheckIssues.length > 0 && ( +
+ {preCheckIssues.map((issue, idx) => ( +
+

{issue.title}

+

{issue.message}

+ {issue.nextStep && ( +

+ {issue.nextStep} +

+ )} +
+ ))} +
+ )} + + {/* Wallet Connected - Preview */} + {connectedWallet && + !networkMismatch && + step === "preview" && ( +
+
+ + Your Wallet + + + {truncateAddress(connectedWallet)} + +
+
+ + USDC Balance + + + {parseFloat(walletInfo?.usdcBalance || 0).toFixed(2)} USDC + +
+
+ Network + {network} +
+ {!walletInfo?.hasTrustline && ( +

+ ⚠ No USDC trustline. Add USDC asset in your wallet first. +

+ )} + {hasInsufficientBalance && ( +

+ Insufficient USDC balance. Please add funds to your wallet. +

+ )} +
+ )} + {/* Confirm Step */} {step === "confirm" && paymentData && (
@@ -214,7 +370,9 @@ export default function PaymentModal({
Creator: - {paymentData.creator.name} + + {paymentData.creator.name} +
@@ -282,14 +440,21 @@ export default function PaymentModal({ {step === "error" && (
-

Payment Failed

+

+ {errorDetail?.title || "Payment Failed"} +

{error}

+ {errorDetail?.nextStep && ( +

+ {errorDetail.nextStep} +

+ )}
)} {/* Action Buttons */}
- {step === "preview" && connectedWallet && ( + {step === "preview" && ( <> - + {connectedWallet && !networkMismatch ? ( + + ) : !connectedWallet ? ( + + ) : null} )} diff --git a/components/stellar/StellarProvider.jsx b/components/stellar/StellarProvider.jsx index bd90baf..1fb8e38 100644 --- a/components/stellar/StellarProvider.jsx +++ b/components/stellar/StellarProvider.jsx @@ -16,8 +16,18 @@ import { AlbedoModule } from "@creit.tech/stellar-wallets-kit/modules/albedo"; import { toast } from "sonner"; import useAuth from "@/hooks/useAuth"; import axiosInstance from "@/lib/config/axios.config"; +import { + isNoWalletError, + isUserRejection, + WALLET_ERRORS, + WALLET_INSTALL_LINKS, +} from "@/lib/stellar/stellarErrors"; const NETWORK = process.env.NEXT_PUBLIC_STELLAR_NETWORK || "testnet"; +const EXPECTED_PASSPHRASE = + NETWORK === "mainnet" + ? Networks.PUBLIC + : Networks.TESTNET; const StellarContext = createContext(null); @@ -36,28 +46,46 @@ export default function StellarProvider({ children }) { const [walletInfo, setWalletInfo] = useState(null); const [isConnecting, setIsConnecting] = useState(false); const [isLoading, setIsLoading] = useState(true); + const [hasWalletExtension, setHasWalletExtension] = useState(false); + const [walletNetwork, setWalletNetwork] = useState(null); + const [networkMismatch, setNetworkMismatch] = useState(false); - // Initialize Stellar Wallets Kit + // Detect wallet extensions and initialize kit useEffect(() => { - try { - // Create modules for different wallets - const modules = [ - new FreighterModule(), - new xBullModule(), - new AlbedoModule(), - ]; - - // Initialize the kit with static method - StellarWalletsKit.init({ - modules, - network: NETWORK === "mainnet" ? Networks.PUBLIC : Networks.TESTNET, - selectedWalletId: FREIGHTER_ID, // Default to Freighter - }); + async function detectWallets() { + try { + const modules = [ + new FreighterModule(), + new xBullModule(), + new AlbedoModule(), + ]; - setKitInitialized(true); - } catch (error) { - console.error("Failed to initialize Stellar Wallets Kit:", error); + StellarWalletsKit.init({ + modules, + network: EXPECTED_PASSPHRASE, + selectedWalletId: FREIGHTER_ID, + }); + + setKitInitialized(true); + + // Check if Freighter is actually installed + try { + if (typeof window !== "undefined" && window.freighter) { + const connected = await window.freighter.isConnected(); + setHasWalletExtension(connected); + } else { + // Check for any wallet by trying to detect + setHasWalletExtension(false); + } + } catch { + setHasWalletExtension(false); + } + } catch (error) { + console.error("Failed to initialize Stellar Wallets Kit:", error); + } } + + detectWallets(); }, []); // Check if user already has connected wallet in database @@ -84,6 +112,32 @@ export default function StellarProvider({ children }) { checkStoredWallet(); }, [user?._id]); + // Check network alignment when wallet connects + useEffect(() => { + if (!connectedWallet || !kitInitialized) return; + + async function checkNetwork() { + try { + const { address } = await StellarWalletsKit.authModal().catch(() => ({})); + // If we can reach this, wallet is available. + // The kit was initialized with the correct network, so if auth succeeds + // the wallet should be on the right network. + setNetworkMismatch(false); + setWalletNetwork(NETWORK); + } catch (error) { + if (!isNoWalletError(error)) { + // Some other issue — could be network mismatch + const msg = error?.message || ""; + if (msg.includes("network") || msg.includes("mismatch")) { + setNetworkMismatch(true); + } + } + } + } + + checkNetwork(); + }, [connectedWallet, kitInitialized]); + // Connect wallet using the auth modal const connectWallet = useCallback(async () => { if (!kitInitialized) { @@ -98,7 +152,6 @@ export default function StellarProvider({ children }) { setIsConnecting(true); try { - // Open the authentication modal which returns the address when successful const { address } = await StellarWalletsKit.authModal(); if (!address) { @@ -113,9 +166,10 @@ export default function StellarProvider({ children }) { if (res.data.success) { setConnectedWallet(address); setWalletInfo(res.data.wallet); + setWalletNetwork(NETWORK); + setNetworkMismatch(false); toast.success("Wallet connected successfully!"); - // Refresh user data if (user?._id) { await refreshUser(user._id); } @@ -123,9 +177,20 @@ export default function StellarProvider({ children }) { throw new Error(res.data.message || "Failed to connect wallet"); } } catch (error) { - // User closing modal is not an error worth showing - if (error?.code === -1 && error?.message?.includes("closed")) { - console.log("User closed wallet modal"); + if (isNoWalletError(error)) { + const installInfo = WALLET_INSTALL_LINKS; + toast.error( + "No Stellar wallet detected. Install Freighter to continue.", + { + duration: 8000, + action: { + label: "Install Freighter", + onClick: () => window.open(installInfo.freighter.url, "_blank"), + }, + } + ); + } else if (isUserRejection(error)) { + // Silent — user cancelled } else { console.error("Wallet connection error:", error); toast.error(error.message || "Failed to connect wallet"); @@ -140,11 +205,12 @@ export default function StellarProvider({ children }) { try { await axiosInstance.delete("/api/stellar/wallet/disconnect"); - // Disconnect from the kit as well StellarWalletsKit.disconnect(); setConnectedWallet(null); setWalletInfo(null); + setWalletNetwork(null); + setNetworkMismatch(false); toast.success("Wallet disconnected"); if (user?._id) { @@ -172,33 +238,88 @@ export default function StellarProvider({ children }) { } }, [connectedWallet]); - // Sign transaction + // Sign transaction with user rejection handling const signTransaction = useCallback( async (xdr, networkPassphrase) => { if (!kitInitialized) { throw new Error("Wallet kit not initialized"); } - const { signedTxXdr } = await StellarWalletsKit.signTransaction(xdr, { - networkPassphrase, - }); + try { + const { signedTxXdr } = await StellarWalletsKit.signTransaction(xdr, { + networkPassphrase, + }); - return signedTxXdr; + return signedTxXdr; + } catch (error) { + if (isUserRejection(error)) { + const rejectionError = new Error(WALLET_ERRORS.user_rejected.message); + rejectionError.code = "USER_REJECTED"; + rejectionError.title = WALLET_ERRORS.user_rejected.title; + rejectionError.nextStep = WALLET_ERRORS.user_rejected.nextStep; + throw rejectionError; + } + throw error; + } }, [kitInitialized] ); + // Validate pre-payment conditions + const validateForPayment = useCallback( + async (price) => { + const issues = []; + + if (!connectedWallet) { + issues.push({ + ...WALLET_ERRORS.wallet_not_installed, + type: "no_wallet", + }); + } + + if (networkMismatch) { + issues.push({ + ...WALLET_ERRORS.network_mismatch, + message: `Your wallet is on the wrong network. Please switch to ${NETWORK}.`, + }); + } + + if (walletInfo && !walletInfo.hasTrustline) { + issues.push(WALLET_ERRORS.trustline_missing); + } + + if ( + connectedWallet && + walletInfo && + parseFloat(walletInfo.usdcBalance || 0) < (price || 0) + ) { + issues.push({ + ...WALLET_ERRORS.insufficient_balance, + message: `You need $${price} USDC but only have $${parseFloat(walletInfo.usdcBalance || 0).toFixed(2)}.`, + }); + } + + return issues; + }, + [connectedWallet, networkMismatch, walletInfo] + ); + const value = { kitInitialized, connectedWallet, walletInfo, isConnecting, isLoading, + hasWalletExtension, + walletNetwork, + networkMismatch, connectWallet, disconnectWallet, refreshBalance, signTransaction, + validateForPayment, network: NETWORK, + expectedNetworkPassphrase: EXPECTED_PASSPHRASE, }; return ( diff --git a/components/stellar/WalletConnectButton.jsx b/components/stellar/WalletConnectButton.jsx index 3de87aa..2bd41a7 100644 --- a/components/stellar/WalletConnectButton.jsx +++ b/components/stellar/WalletConnectButton.jsx @@ -10,8 +10,18 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Badge } from "@/components/ui/badge"; -import { Wallet, LogOut, RefreshCw, ExternalLink, Copy, Loader2 } from "lucide-react"; +import { + Wallet, + LogOut, + RefreshCw, + ExternalLink, + Copy, + Loader2, + AlertTriangle, + Download, +} from "lucide-react"; import { toast } from "sonner"; +import { WALLET_INSTALL_LINKS } from "@/lib/stellar/stellarErrors"; export default function WalletConnectButton({ variant = "outline", size = "default" }) { const { @@ -23,6 +33,8 @@ export default function WalletConnectButton({ variant = "outline", size = "defau disconnectWallet, refreshBalance, network, + hasWalletExtension, + networkMismatch, } = useStellar(); const truncateAddress = (addr) => { @@ -43,7 +55,6 @@ export default function WalletConnectButton({ variant = "outline", size = "defau window.open(baseUrl + connectedWallet, "_blank"); }; - // Show loading state if (isLoading) { return ( + + + +

+ No Stellar wallet detected. Install one to continue: +

+
+ + window.open(WALLET_INSTALL_LINKS.freighter.url, "_blank")} + className="cursor-pointer" + > + + Install Freighter + + window.open(WALLET_INSTALL_LINKS.xbull.url, "_blank")} + className="cursor-pointer" + > + + Install xBull + + window.open(WALLET_INSTALL_LINKS.albedo.url, "_blank")} + className="cursor-pointer" + > + + Use Albedo (web) + + + + + Already installed? Connect + +
+ + ); + } + + // Not connected — show connect button if (!connectedWallet) { return (
+ {networkMismatch && ( +
+ + + Wrong network. Switch wallet to {network}. + +
+ )}
Network @@ -108,6 +179,12 @@ export default function WalletConnectButton({ variant = "outline", size = "defau {parseFloat(walletInfo?.xlmBalance || 0).toFixed(4)} XLM
+ {walletInfo && !walletInfo.hasTrustline && ( +
+ + USDC trustline not set up. +
+ )}
diff --git a/hooks/useStellarPayment.js b/hooks/useStellarPayment.js index e475deb..e60fa2b 100644 --- a/hooks/useStellarPayment.js +++ b/hooks/useStellarPayment.js @@ -4,20 +4,18 @@ import { useStellar } from "@/components/stellar/StellarProvider"; import { toast } from "sonner"; import axiosInstance from "@/lib/config/axios.config"; import useAuth from "./useAuth"; +import { + mapStellarError, + isUserRejection, + WALLET_ERRORS, +} from "@/lib/stellar/stellarErrors"; -/** - * Hook for handling Stellar payment operations - */ export const useStellarPayment = () => { const { connectedWallet, signTransaction, refreshBalance } = useStellar(); const { refreshUser, user } = useAuth(); const [isProcessing, setIsProcessing] = useState(false); const [currentTransaction, setCurrentTransaction] = useState(null); - /** - * Initialize a payment by creating a pending transaction - * Returns the XDR to be signed by the wallet - */ const initializePayment = useCallback( async ({ itemType, itemId }) => { if (!connectedWallet) { @@ -40,8 +38,15 @@ export const useStellarPayment = () => { throw new Error(res.data.message); } } catch (error) { - const message = error.response?.data?.message || error.message; - toast.error(message); + const mapped = mapStellarError(error); + if (mapped) { + toast.error(mapped.message, { + description: mapped.nextStep, + }); + } else { + const message = error.response?.data?.message || error.message; + toast.error(message); + } return null; } finally { setIsProcessing(false); @@ -50,9 +55,6 @@ export const useStellarPayment = () => { [connectedWallet] ); - /** - * Execute payment by signing and submitting the transaction - */ const executePayment = useCallback( async (paymentData) => { if (!paymentData) { @@ -62,13 +64,11 @@ export const useStellarPayment = () => { setIsProcessing(true); try { - // Sign the transaction with wallet const signedXdr = await signTransaction( paymentData.payment.xdr, paymentData.payment.networkPassphrase ); - // Submit to backend const res = await axiosInstance.post("/api/stellar/payment/submit", { transactionId: paymentData.transactionId, signedXdr, @@ -77,32 +77,31 @@ export const useStellarPayment = () => { if (res.data.success) { toast.success("Payment successful!"); - // Refresh user data to update purchased items if (user?._id) { await refreshUser(user._id); } - // Refresh wallet balance await refreshBalance(); - setCurrentTransaction(null); return res.data; } else { throw new Error(res.data.message); } } catch (error) { - const message = error.response?.data?.message || error.message; - - // Handle specific Stellar errors - if (message.includes("insufficient") || message.includes("underfunded")) { - toast.error("Insufficient USDC balance"); - } else if (message.includes("op_no_trust") || message.includes("trustline")) { - toast.error( - "You need to add USDC trustline to your wallet first" - ); - } else if (message.includes("rejected") || message.includes("cancelled")) { - toast.error("Transaction was cancelled"); + if (isUserRejection(error) || error.code === "USER_REJECTED") { + toast.info("Transaction cancelled", { + description: "You declined the signing request. No changes were made.", + }); + return false; + } + + const mapped = mapStellarError(error); + if (mapped) { + toast.error(mapped.title, { + description: mapped.nextStep, + }); } else { + const message = error.response?.data?.message || error.message; toast.error(`Payment failed: ${message}`); } return false; @@ -113,9 +112,6 @@ export const useStellarPayment = () => { [signTransaction, user, refreshUser, refreshBalance] ); - /** - * Cancel the current pending transaction - */ const cancelPayment = useCallback(async () => { if (currentTransaction?.transactionId) { try { @@ -129,9 +125,6 @@ export const useStellarPayment = () => { setCurrentTransaction(null); }, [currentTransaction]); - /** - * Get user's transaction history - */ const getTransactionHistory = useCallback( async ({ role = "buyer", page = 1, limit = 20 } = {}) => { try { @@ -147,9 +140,6 @@ export const useStellarPayment = () => { [] ); - /** - * Get a single transaction's details - */ const getTransaction = useCallback(async (transactionId) => { try { const res = await axiosInstance.get( diff --git a/lib/stellar/stellarErrors.js b/lib/stellar/stellarErrors.js new file mode 100644 index 0000000..d0152b9 --- /dev/null +++ b/lib/stellar/stellarErrors.js @@ -0,0 +1,181 @@ +/** + * Reusable Stellar error mapping utility. + * Maps raw SDK/operation errors to human-readable messages with next-step guidance. + */ + +const STELLAR_ERRORS = { + op_no_trust: { + title: "USDC Trustline Required", + message: "Your wallet doesn't have a USDC trustline set up.", + nextStep: "Open your wallet and add the USDC asset before making payments.", + type: "trustline", + }, + op_underfunded: { + title: "Insufficient Balance", + message: "You don't have enough USDC to complete this transaction.", + nextStep: "Add more USDC to your wallet and try again.", + type: "balance", + }, + op_no_destination: { + title: "Invalid Recipient", + message: "The recipient account doesn't exist on Stellar.", + nextStep: "This is a platform issue. Please contact support.", + type: "unknown", + }, + op_low_reserve: { + title: "Insufficient XLM Reserve", + message: "Your account doesn't have enough XLM to cover the base reserve.", + nextStep: "Add more XLM to your wallet for transaction fees.", + type: "balance", + }, + tx_bad_auth: { + title: "Authentication Failed", + message: "The transaction signature doesn't match.", + nextStep: "Try reconnecting your wallet and signing again.", + type: "auth", + }, + tx_bad_seq: { + title: "Transaction Sequence Error", + message: "The transaction sequence number doesn't match your account.", + nextStep: "Refresh your wallet and try the payment again.", + type: "sequence", + }, + tx_insufficient_fee: { + title: "Insufficient Fee", + message: "The transaction fee is too low.", + nextStep: "The platform will handle fees. Please try again.", + type: "fee", + }, + tx_not_supported: { + title: "Transaction Not Supported", + message: "This transaction type is not supported.", + nextStep: "Please contact support.", + type: "unknown", + }, +}; + +const WALLET_ERRORS = { + user_rejected: { + title: "Transaction Cancelled", + message: "You declined the signing request in your wallet.", + nextStep: "No action needed. Your wallet is safe.", + type: "reject", + }, + wallet_not_installed: { + title: "No Wallet Found", + message: "No Stellar wallet extension detected in your browser.", + nextStep: "Install Freighter to get started.", + type: "no_wallet", + }, + network_mismatch: { + title: "Wrong Network", + message: "Your wallet is on a different network than DeenBridge.", + nextStep: "Switch your wallet to testnet (or mainnet) and try again.", + type: "network", + }, + trustline_missing: { + title: "USDC Trustline Missing", + message: "You need to add a USDC trustline before making payments.", + nextStep: "Open your wallet, go to assets, and add the USDC token.", + type: "trustline", + }, + insufficient_balance: { + title: "Insufficient USDC", + message: "Your USDC balance is too low for this purchase.", + nextStep: "Add USDC to your wallet and try again.", + type: "balance", + }, +}; + +const WALLET_INSTALL_LINKS = { + freighter: { + name: "Freighter", + url: "https://www.freighter.app/", + description: "The most popular Stellar wallet browser extension", + }, + xbull: { + name: "xBull", + url: "https://xbull.app/", + description: "A secure Stellar wallet", + }, + albedo: { + name: "Albedo", + url: "https://albedo.link/", + description: "Web-based Stellar wallet (no extension needed)", + }, +}; + +/** + * Map a raw error string/code to a user-friendly Stellar error object. + */ +export function mapStellarError(rawError) { + if (!rawError) return null; + + const errorStr = typeof rawError === "string" ? rawError : rawError.message || ""; + + // Check Stellar operation errors + for (const [code, mapped] of Object.entries(STELLAR_ERRORS)) { + if (errorStr.includes(code)) { + return mapped; + } + } + + // Check wallet/signing errors + if ( + errorStr.includes("rejected") || + errorStr.includes("cancelled") || + errorStr.includes("declined") || + errorStr.includes("denied") + ) { + return WALLET_ERRORS.user_rejected; + } + + if (errorStr.includes("insufficient") || errorStr.includes("underfunded")) { + return WALLET_ERRORS.insufficient_balance; + } + + if (errorStr.includes("op_no_trust") || errorStr.includes("trustline")) { + return WALLET_ERRORS.trustline_missing; + } + + if (errorStr.includes("network") || errorStr.includes("mismatch")) { + return WALLET_ERRORS.network_mismatch; + } + + return null; +} + +/** + * Check if a StellarKit auth modal error means no wallet was found. + */ +export function isNoWalletError(error) { + if (!error) return false; + const msg = typeof error === "string" ? error : error.message || ""; + return ( + msg.includes("no wallet") || + msg.includes("no extension") || + msg.includes("not installed") || + msg.includes("undefined") || + msg.includes("Cannot read") || + error.code === -1 + ); +} + +/** + * Check if a signing error means the user rejected/cancelled. + */ +export function isUserRejection(error) { + if (!error) return false; + const msg = typeof error === "string" ? error : error.message || ""; + return ( + msg.includes("rejected") || + msg.includes("cancelled") || + msg.includes("declined") || + msg.includes("denied") || + msg.includes("User declined") || + error.code === 4001 || + error.code === -1 + ); +} + +export { STELLAR_ERRORS, WALLET_ERRORS, WALLET_INSTALL_LINKS };