diff --git a/frontend/src/components/ZapFlow.tsx b/frontend/src/components/ZapFlow.tsx index 7fe2378..2a3c569 100644 --- a/frontend/src/components/ZapFlow.tsx +++ b/frontend/src/components/ZapFlow.tsx @@ -1,14 +1,11 @@ import { useState, useMemo } from 'react'; -import { useAccount, useWriteContract } from 'wagmi'; +import { useAccount, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'; import { parseUnits, encodeFunctionData } from 'viem'; import { openContractCall } from '@stacks/connect'; import { Pc, Cl } from '@stacks/transactions'; import { useAppStore } from '../stores/appStore'; import { useToast } from '../contexts/ToastContext'; -import { useGasEstimation } from '../hooks/useGasEstimation'; import { useUSDCBalance } from '../hooks/useUSDCBalance'; -import { GasFeeDisplay } from './GasFeeDisplay'; -import { BalanceDisplay } from './BalanceDisplay'; import { SEPOLIA_USDC, SEPOLIA_XRESERVE, @@ -18,386 +15,199 @@ import { } from '../lib/constants'; import { encodeStacksAddressForXReserve } from '../lib/stacksAddressEncoder'; -interface ZapFlowProps { - strategyName: string; - onClose: () => void; -} +type Step = 'input' | 'approve' | 'bridge' | 'finalize' | 'complete'; export function ZapFlow({ strategyName, onClose }: ZapFlowProps) { const [amount, setAmount] = useState(''); - const [step, setStep] = useState<'input' | 'approve' | 'deposit' | 'bridge' | 'finalize'>('input'); + const [step, setStep] = useState('input'); const [error, setError] = useState(null); + const [isProcessing, setIsProcessing] = useState(false); const [txHash, setTxHash] = useState(null); - + const { address: ethAddress } = useAccount(); const { stacksWallet, setZapState } = useAppStore(); - const { writeContract } = useWriteContract(); const toast = useToast(); - const { balance, formattedBalance, isLoading: balanceLoading, error: balanceError, refetch: refetchBalance } = useUSDCBalance(); - - // Prepare approval transaction data for gas estimation - const approvalData = useMemo(() => { - if (!amount) return undefined; - try { - const amountWei = parseUnits(amount, 6); - return encodeFunctionData({ - abi: ERC20_ABI, - functionName: 'approve', - args: [SEPOLIA_XRESERVE, amountWei], - }); - } catch { - return undefined; - } - }, [amount]); + const { balance, formattedBalance } = useUSDCBalance(); - const approvalGas = useGasEstimation( - step === 'input' && amount ? SEPOLIA_USDC : undefined, - approvalData - ); + const { writeContractAsync } = useWriteContract(); - // Prepare deposit transaction data for gas estimation - const depositData = useMemo(() => { - if (!amount || !stacksWallet) return undefined; + const amountWei = useMemo(() => { try { - const amountWei = parseUnits(amount, 6); - // Properly encode Stacks address for xReserve bridge - const stacksAddressBytes = encodeStacksAddressForXReserve(stacksWallet.address); - return encodeFunctionData({ - abi: XRESERVE_ABI, - functionName: 'depositForBurn', - args: [amountWei, stacksAddressBytes], - }); + return amount ? parseUnits(amount, 6) : undefined; } catch { return undefined; } - }, [amount, stacksWallet]); - - const depositGas = useGasEstimation( - step === 'approve' && amount ? SEPOLIA_XRESERVE : undefined, - depositData - ); + }, [amount]); - // Check if amount exceeds balance - const hasInsufficientBalance = useMemo(() => { - if (!amount || !balance) return false; - try { - const amountWei = parseUnits(amount, 6); - return amountWei > balance; - } catch { - return false; - } - }, [amount, balance]); + const isValidAmount = useMemo(() => { + if (!amountWei) return false; + if (!balance) return false; + return amountWei > 0n && amountWei <= balance; + }, [amountWei, balance]); const handleApprove = async () => { - if (!amount || !ethAddress) return; - - const numAmount = parseFloat(amount); - - // Validate minimum amount - if (numAmount < 0.01) { - const minMsg = 'Minimum amount is 0.01 USDC'; - setError(minMsg); - toast.showError(minMsg); - return; - } - - // Check if user has sufficient balance - const amountWei = parseUnits(amount, 6); - if (amountWei > balance) { - const insufficientMsg = `Insufficient balance. You have ${formattedBalance} USDC but need ${amount} USDC`; - setError(insufficientMsg); - toast.showError(insufficientMsg); - return; - } - + if (!amountWei || !ethAddress) return; + try { + setIsProcessing(true); setError(null); setStep('approve'); - const loadingToast = toast.showLoading('Approving USDC...'); - - writeContract({ + + const hash = await writeContractAsync({ address: SEPOLIA_USDC, abi: ERC20_ABI, functionName: 'approve', args: [SEPOLIA_XRESERVE, amountWei], }); - - toast.dismissToast(loadingToast); - toast.showSuccess('USDC approval successful!'); - // Refetch balance after approval - setTimeout(() => refetchBalance(), 2000); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : 'Approval failed'; - console.error('Approval failed:', error); - setError(errorMsg); - setZapState({ status: 'error', error: errorMsg }); - toast.showError(errorMsg); + + toast.showSuccess('Approval submitted'); + setZapState({ status: 'approved', txHash: hash }); + + setStep('bridge'); + } catch (err: any) { + setError(err.message || 'Approval failed'); setStep('input'); + } finally { + setIsProcessing(false); } }; const handleDeposit = async () => { - if (!amount || !ethAddress || !stacksWallet) return; - + if (!amountWei || !stacksWallet) return; + try { - setError(null); - setStep('deposit'); - setZapState({ status: 'depositing' }); - const loadingToast = toast.showLoading('Depositing to bridge...'); - - const amountWei = parseUnits(amount, 6); - - // Properly encode Stacks address to bytes32 for xReserve bridge - // Uses proper c32 decoding and version/hash160 extraction - const stacksAddressBytes = encodeStacksAddressForXReserve(stacksWallet.address); - - writeContract({ + setIsProcessing(true); + + const stacksAddressBytes = + encodeStacksAddressForXReserve(stacksWallet.address); + + await writeContractAsync({ address: SEPOLIA_XRESERVE, abi: XRESERVE_ABI, functionName: 'depositForBurn', args: [amountWei, stacksAddressBytes], }); - - toast.dismissToast(loadingToast); - toast.showSuccess('Deposit initiated! Bridging to Stacks...'); - // Refetch balance after deposit - setTimeout(() => refetchBalance(), 2000); - setStep('bridge'); - setZapState({ - status: 'bridging', - amount, - stacksAddress: stacksWallet.address, - }); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : 'Deposit failed'; - console.error('Deposit failed:', error); - setError(errorMsg); - setZapState({ status: 'error', error: errorMsg }); - toast.showError(errorMsg); - setStep('approve'); + + toast.showSuccess('Bridge deposit submitted'); + setStep('finalize'); + } catch (err: any) { + setError(err.message || 'Deposit failed'); + } finally { + setIsProcessing(false); } }; const handleFinalize = async () => { - if (!stacksWallet || !amount) return; - + if (!amountWei || !stacksWallet) return; + try { - setError(null); - setStep('finalize'); - setZapState({ status: 'finalizing' }); - const loadingToast = toast.showLoading('Finalizing deposit on Stacks...'); - - const amountMicroUnits = BigInt(parseFloat(amount) * 1_000_000); - - // Post-condition: ensure USDCx transfer - const postConditions = [ - Pc.principal(stacksWallet.address) - .willSendEq(Number(amountMicroUnits)) - .ft('ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usdcx', 'usdcx'), - ]; - + setIsProcessing(true); + await openContractCall({ network: 'testnet', contractAddress: STACKS_MOCK_VAULT.split('.')[0], contractName: STACKS_MOCK_VAULT.split('.')[1], functionName: 'deposit', - functionArgs: [Cl.uint(amountMicroUnits)], - postConditions, + functionArgs: [Cl.uint(amountWei)], + postConditions: [ + Pc.principal(stacksWallet.address) + .willSendEq(Number(amountWei)) + .ft('ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usdcx', 'usdcx'), + ], onFinish: (data: any) => { - console.log('Stacks tx:', data.txId); setTxHash(data.txId); - setZapState({ status: 'complete' }); - toast.dismissToast(loadingToast); - toast.showTransactionSuccess( - data.txId, - 'https://explorer.hiro.so/txid/' - ); - }, - onCancel: () => { - const cancelMsg = 'Transaction cancelled by user'; - setError(cancelMsg); - setZapState({ status: 'error', error: cancelMsg }); - toast.dismissToast(loadingToast); - toast.showError(cancelMsg); - setStep('bridge'); + setStep('complete'); + toast.showSuccess('Zap complete'); }, }); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : 'Failed to finalize deposit'; - console.error('Finalize failed:', error); - setError(errorMsg); - setZapState({ status: 'error', error: errorMsg }); - toast.showError(errorMsg); - setStep('bridge'); + } catch (err: any) { + setError(err.message || 'Finalize failed'); + } finally { + setIsProcessing(false); } }; + const stepOrder: Step[] = ['input', 'approve', 'bridge', 'finalize', 'complete']; + const currentIndex = stepOrder.indexOf(step); + return ( -
-
-
-

Zap to {strategyName}

- -
+
+
+

+ Zap to {strategyName} +

- {/* Progress Steps */} + {/* Progress */}
- {['Input', 'Approve', 'Bridge', 'Finalize'].map((label, idx) => ( -
-
= idx - ? 'bg-blue-500' - : 'bg-slate-600' - }`} - > - {idx + 1} -
-
{label}
+ {stepOrder.slice(0, 4).map((label, idx) => ( +
= idx ? 'bg-blue-500' : 'bg-slate-600' + }`} + > + {idx + 1}
))}
- {/* Error Display */} {error && ( -
-

{error}

+
+ {error}
)} - {/* Input Amount */} {step === 'input' && ( -
-
- -
- setAmount(e.target.value)} - placeholder="0.00" - className="w-full px-4 py-3 bg-slate-900 border border-slate-600 rounded-lg focus:border-blue-500 focus:outline-none pr-20" - /> - -
-
- -
-

• Connected: {ethAddress?.slice(0, 6)}...{ethAddress?.slice(-4)}

-

• Stacks: {stacksWallet?.address.slice(0, 6)}...{stacksWallet?.address.slice(-4)}

-
- - + setAmount(e.target.value)} + placeholder="0.00" + className="w-full px-4 py-3 bg-slate-900 rounded-lg" /> - {hasInsufficientBalance && ( -
-

- ⚠️ Amount exceeds your balance -

-
- )} - - {amount && ( - - )} - -
+ )} - {/* Approve Step */} {step === 'approve' && ( -
-

Approve USDC spending on Ethereum...

- - - - -
+ )} - {/* Bridge Step */} {step === 'bridge' && ( -
-
-
-

Bridging to Stacks...

-

- This may take 5-20 minutes. Circle is attesting your deposit. -

-
- -
+ )} - {/* Finalize Step */} - {step === 'finalize' && ( -
-
-

Zap Complete!

-

- Your funds are now earning {strategyName} yield on Stacks. -

+ {step === 'complete' && ( +
+

✓ Zap Complete

{txHash && ( -
-

Transaction Hash:

- - {txHash} - -
+

{txHash}

)}
)}