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
2 changes: 1 addition & 1 deletion packages/snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "Hathor Network Snap integration",
"proposedName": "Hathor Wallet",
"source": {
"shasum": "cVvK+qMhEy0PYL6JKlfwQpWXe0gCJ2oyU5THdlb8jLc=",
"shasum": "UP48HeVrcHOdxuR/5bm9Iy6aa8u2Wxoo9pX5MvRwsc4=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
9 changes: 8 additions & 1 deletion packages/snap/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,16 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
return bigIntUtils.JSONBigInt.stringify(response);
} catch (e: any) {
// Re-throw using SnapError to properly serialize the data property
// Include stack trace and error type for debugging in the web-wallet
const errorData = {
...e.data,
errorType: e.name || e.data?.errorType || 'UnknownError',
stack: e.stack || undefined,
};

const snapError = new SnapError(
e.message || 'Unknown error',
e.data || { errorType: e.name || 'UnknownError' }
errorData
);
// Try to preserve the original error code
if (e.code) {
Expand Down
41 changes: 29 additions & 12 deletions packages/web-wallet/src/components/CreateTokenDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getAddressForMode } from '../utils/addressMode';
// TODO: Re-enable when fee token feature is ready
// import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { useToast } from '@/hooks/use-toast';
import TransactionErrorDisplay from './TransactionErrorDisplay';

interface CreateTokenDialogProps {
isOpen: boolean;
Expand Down Expand Up @@ -95,7 +96,7 @@ type CreateTokenFormData = z.infer<typeof createTokenSchema>;

const CreateTokenDialog: React.FC<CreateTokenDialogProps> = ({ isOpen, onClose }) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [error, setError] = useState<Error | null>(null);
const [successData, setSuccessData] = useState<{
configString: string;
tokenName: string;
Expand Down Expand Up @@ -253,21 +254,40 @@ const CreateTokenDialog: React.FC<CreateTokenDialogProps> = ({ isOpen, onClose }
tokenName: data.name,
tokenSymbol: data.symbol,
});
} catch (err) {
} catch (err: unknown) {
console.error('Failed to create token:', err);

// User-friendly error messages
const errorMsg = err instanceof Error ? err.message : 'Failed to create token';
// Snap errors come as objects with message/data properties, not Error instances
const snapErr = err as { message?: string; data?: { errorType?: string; stack?: string }; name?: string; stack?: string };
const errorMsg = snapErr?.message || 'Failed to create token';

// Create user-friendly error with appropriate type
let userError: Error;
if (errorMsg.includes('rejected') || errorMsg.includes('User rejected')) {
setError('Transaction was cancelled. Please try again.');
userError = new Error('Transaction was cancelled. Please try again.');
userError.name = 'UserRejectedError';
} else if (errorMsg.includes('insufficient') || errorMsg.includes('Insufficient')) {
setError('Insufficient HTR balance for deposit and fees.');
userError = new Error('Insufficient HTR balance for deposit and fees.');
userError.name = 'InsufficientBalanceError';
} else if (errorMsg.includes('timeout') || errorMsg.includes('timed out')) {
setError('Request timed out. Please check your connection and try again.');
userError = new Error('Request timed out. Please check your connection and try again.');
userError.name = 'TimeoutError';
} else {
setError(errorMsg);
userError = new Error(errorMsg);
userError.name = snapErr?.data?.errorType || snapErr?.name || 'Error';
}

// Preserve stack trace from snap error data
if (snapErr?.data?.stack) {
userError.stack = snapErr.data.stack;
} else if (snapErr?.stack) {
userError.stack = snapErr.stack;
}

// Attach the original data for TransactionErrorDisplay to extract
(userError as any).data = snapErr?.data;

setError(userError);
} finally {
setIsLoading(false);
}
Expand Down Expand Up @@ -601,10 +621,7 @@ const CreateTokenDialog: React.FC<CreateTokenDialogProps> = ({ isOpen, onClose }

{/* Error Message */}
{error && (
<div className="flex items-start gap-2 p-3 bg-red-500/10 border border-red-500/50 rounded-lg">
<AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0 mt-0.5" />
<span className="text-red-400 text-sm">{error}</span>
</div>
<TransactionErrorDisplay error={error} />
)}

{/* Create Button */}
Expand Down
129 changes: 129 additions & 0 deletions packages/web-wallet/src/components/ErrorDetailsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import React from 'react';
import { Copy } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from './ui/dialog';
import { Button } from './ui/button';

export interface ErrorDetails {
type: string;
message: string;
timestamp: Date;
stack?: string;
}

interface ErrorDetailsModalProps {
isOpen: boolean;
onClose: () => void;
error: ErrorDetails;
}

const ErrorDetailsModal: React.FC<ErrorDetailsModalProps> = ({
isOpen,
onClose,
error,
}) => {
const { toast } = useToast();

const formatTimestamp = (date: Date) => {
return date.toLocaleString(undefined, {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
});
};

const getErrorText = () => {
const lines = [
`Error Type: ${error.type}`,
`Message: ${error.message}`,
`Timestamp: ${formatTimestamp(error.timestamp)}`,
];
if (error.stack) {
lines.push('', 'Stack Trace:', error.stack);
}
return lines.join('\n');
};

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(getErrorText());
toast({
variant: 'success',
title: 'Error details copied to clipboard',
});
} catch (err) {
console.error('Failed to copy error details:', err);
toast({
variant: 'destructive',
title: 'Failed to copy to clipboard',
});
}
};

return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-md max-h-[80vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Error Details
</DialogTitle>
</DialogHeader>

<div className="flex-1 overflow-y-auto space-y-4 pr-2">
{/* Error Type */}
<div>
<h4 className="text-sm font-bold text-white mb-1">Error Type</h4>
<p className="text-sm text-muted-foreground">{error.type}</p>
</div>

{/* Message */}
<div>
<h4 className="text-sm font-bold text-white mb-1">Message</h4>
<p className="text-sm text-muted-foreground break-words">{error.message}</p>
</div>

{/* Timestamp */}
<div>
<h4 className="text-sm font-bold text-white mb-1">Timestamp</h4>
<p className="text-sm text-muted-foreground">{formatTimestamp(error.timestamp)}</p>
</div>

{/* Stack Trace */}
{error.stack && (
<div>
<h4 className="text-sm font-bold text-white mb-1">Stack Trace</h4>
<div className="bg-[#0D1117] border border-border rounded-lg p-3 max-h-40 overflow-y-auto">
<pre className="text-xs text-muted-foreground whitespace-pre-wrap break-all font-mono">
{error.stack}
</pre>
</div>
</div>
)}
</div>

{/* Footer */}
<div className="flex justify-end gap-2 pt-4 border-t border-border mt-4">
<Button
variant="outline"
onClick={handleCopy}
className="flex items-center gap-2 hover:bg-primary/20 hover:text-primary hover:border-primary"
>
<Copy className="w-4 h-4" />
Copy to Clipboard
</Button>
<Button onClick={onClose}>Close</Button>
</div>
</DialogContent>
</Dialog>
);
};

export default ErrorDetailsModal;
50 changes: 31 additions & 19 deletions packages/web-wallet/src/components/SendDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { TOKEN_IDS } from '../constants';
import { readOnlyWalletWrapper } from '../services/ReadOnlyWalletWrapper';
import { getAddressForMode } from '../utils/addressMode';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import TransactionErrorDisplay from './TransactionErrorDisplay';

interface SendDialogProps {
isOpen: boolean;
Expand Down Expand Up @@ -85,7 +86,7 @@ type SendFormData = z.infer<ReturnType<typeof createSendFormSchema>>;
const SendDialog: React.FC<SendDialogProps> = ({ isOpen, onClose, initialTokenUid }) => {
const [showAdvanced, setShowAdvanced] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [transactionError, setTransactionError] = useState<string | null>(null);
const [transactionError, setTransactionError] = useState<Error | null>(null);

const { sendTransaction, network, addressMode } = useWallet();
const { allTokens } = useTokens();
Expand Down Expand Up @@ -214,11 +215,13 @@ const SendDialog: React.FC<SendDialogProps> = ({ isOpen, onClose, initialTokenUi
? shortfall.toString()
: centsToAmount(shortfall);

setTransactionError(
const insufficientBalanceError = new Error(
`Insufficient balance. You need ${displayAmount} ${selectedToken?.symbol} ` +
`but only have ${displayAvailable} ${selectedToken?.symbol} available. ` +
`Short by ${displayShortfall} ${selectedToken?.symbol}.`
);
insufficientBalanceError.name = 'InsufficientBalanceError';
setTransactionError(insufficientBalanceError);
setIsLoading(false);
return;
}
Expand Down Expand Up @@ -271,8 +274,25 @@ const SendDialog: React.FC<SendDialogProps> = ({ isOpen, onClose, initialTokenUi
reset();
setTransactionError(null);
onClose();
} catch (err) {
setTransactionError(err instanceof Error ? err.message : 'Failed to send transaction');
} catch (err: unknown) {
// Snap errors come as objects with message/data properties, not Error instances
// We need to preserve the snap error data (errorType, stack) for display
const snapErr = err as { message?: string; data?: { errorType?: string; stack?: string }; name?: string; stack?: string };

const error = new Error(snapErr?.message || 'Failed to send transaction');
error.name = snapErr?.data?.errorType || snapErr?.name || 'Error';

// Preserve stack trace from snap error data, or from the error itself
if (snapErr?.data?.stack) {
error.stack = snapErr.data.stack;
} else if (snapErr?.stack) {
error.stack = snapErr.stack;
}

// Attach the original data for TransactionErrorDisplay to extract
(error as any).data = snapErr?.data;

setTransactionError(error);
} finally {
setIsLoading(false);
}
Expand Down Expand Up @@ -379,21 +399,13 @@ const SendDialog: React.FC<SendDialogProps> = ({ isOpen, onClose, initialTokenUi
</div>

{transactionError && (
<div className="flex flex-col gap-2 p-3 bg-red-500/10 border border-red-500/50 rounded-lg">
<span className="text-red-400 text-sm whitespace-pre-line">{transactionError}</span>
{transactionError.includes('permission') && (
<button
type="button"
onClick={() => {
onClose();
window.location.reload();
}}
className="text-xs text-primary hover:text-primary/80 underline self-start"
>
Click here to refresh and reconnect
</button>
)}
</div>
<TransactionErrorDisplay
error={transactionError}
onRefreshClick={() => {
onClose();
window.location.reload();
}}
/>
)}

{/* Advanced Options */}
Expand Down
Loading
Loading