Reducing On-Chain Interaction Anxiety Through Frontend Optimization
Reducing On-Chain Interaction Anxiety Through Frontend Optimization
"I clicked approve and nothing happened for 30 seconds. Did I just lose my money?"
This is the most common support ticket in DeFi. Here's how to eliminate that anxiety.
The Anxiety Points
- Pre-transaction: "Will this do what I think it does?"
- During transaction: "Is it working? How long will it take?"
- Post-transaction: "Did it succeed? Where are my funds?"
Solution 1: Transaction Preview
Show exactly what will happen BEFORE the user signs:
function TransactionPreview({ action, details }: Props) {
return (
<div className="bg-surface-elevated border border-surface-border
rounded-xl p-4 mb-4">
<h4 className="text-sm font-semibold text-gray-300 mb-3">
Transaction Preview
</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-500">Action</span>
<span className="text-white font-medium">{action}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">You Pay</span>
<span className="text-red-400">
-{details.amount} USDC
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">You Receive</span>
<span className="text-emerald-400">
Strategy: {details.strategyName}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Protection</span>
<span className="text-blue-400">
72h escrow + AI arbitration
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Est. Gas</span>
<span className="text-gray-300">
~${details.estimatedGas.toFixed(2)}
</span>
</div>
</div>
<div className="mt-3 p-2 bg-blue-600/10 border border-blue-500/20
rounded-lg text-xs text-blue-400">
Your USDC will be locked in the escrow smart contract
(not sent to the seller) until you confirm or the 72-hour
window expires.
</div>
</div>
);
}
Solution 2: Real-Time Transaction Status
function TransactionTracker({ txHash, expectedConfirmations = 2 }: Props) {
const [status, setStatus] = useState<'pending' | 'confirming' | 'confirmed' | 'failed'>('pending');
const [confirmations, setConfirmations] = useState(0);
const [elapsed, setElapsed] = useState(0);
useEffect(() => {
const timer = setInterval(() => setElapsed(e => e + 1), 1000);
return () => clearInterval(timer);
}, []);
return (
<div className="bg-surface-card border border-surface-border
rounded-xl p-5">
{/* Animated status indicator */}
<div className="flex items-center gap-3 mb-4">
{status === 'pending' && (
<div className="w-3 h-3 rounded-full bg-amber-400 animate-pulse" />
)}
{status === 'confirming' && (
<div className="w-3 h-3 rounded-full bg-blue-400 animate-pulse" />
)}
{status === 'confirmed' && (
<div className="w-3 h-3 rounded-full bg-emerald-400" />
)}
<span className="text-sm text-gray-300">
{status === 'pending' && 'Waiting for network...'}
{status === 'confirming' && `Confirming (${confirmations}/${expectedConfirmations})...`}
{status === 'confirmed' && 'Transaction confirmed!'}
</span>
<span className="text-xs text-gray-600 ml-auto">
{elapsed}s
</span>
</div>
{/* Progress bar */}
<div className="h-1.5 bg-surface-elevated rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-500 ${
status === 'confirmed' ? 'bg-emerald-500' : 'bg-blue-500'
}`}
style={{
width: status === 'confirmed' ? '100%'
: `${(confirmations / expectedConfirmations) * 100}%`,
}}
/>
</div>
{/* Etherscan link */}
{txHash && (
<a
href={`https://etherscan.io/tx/${txHash}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-400 hover:text-blue-300 mt-3
inline-block"
>
View on Etherscan ↗
</a>
)}
</div>
);
}
Solution 3: Error Recovery
function TransactionError({ error, onRetry }: Props) {
// Parse common wallet errors into human language
const getMessage = (err: string) => {
if (err.includes('user rejected'))
return { title: 'Transaction Cancelled',
desc: 'You declined the transaction in your wallet.',
action: 'Try Again' };
if (err.includes('insufficient funds'))
return { title: 'Insufficient Balance',
desc: 'You need more ETH for gas or USDC for the purchase.',
action: 'Add Funds' };
if (err.includes('nonce'))
return { title: 'Transaction Conflict',
desc: 'A previous transaction is still pending.',
action: 'Wait & Retry' };
return { title: 'Transaction Failed',
desc: err.slice(0, 200),
action: 'Try Again' };
};
const msg = getMessage(error);
return (
<div className="bg-red-600/10 border border-red-500/20
rounded-xl p-4">
<h4 className="text-red-400 font-semibold">{msg.title}</h4>
<p className="text-sm text-gray-400 mt-1">{msg.desc}</p>
<button
onClick={onRetry}
className="mt-3 text-sm text-red-400 hover:text-red-300
underline underline-offset-2"
>
{msg.action}
</button>
</div>
);
}
These patterns are the result of real user testing on ClawDUX, where first-time Web3 users need to feel confident locking USDC into an escrow smart contract.
The core logic discussed in this article has been integrated into the ClawDUX API. Access ClawDUX-core for full permissions, or browse the marketplace to discover verified trading strategies.