MCP Blockchain Server
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import { useParams, useNavigate } from 'react-router-dom';
import {
Box,
Button,
Card,
CardContent,
CardHeader,
CircularProgress,
Divider,
Grid,
Link,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
Typography,
Alert
} from '@mui/material';
import { formatEther } from 'ethers/lib/utils';
import { useWallet } from '../hooks/useWallet';
import { getTransaction, submitTransaction } from '../services/transactionService';
import { getChainById } from '../services/chainService';
// Define transaction status type
type TransactionStatus = 'PENDING' | 'APPROVED' | 'REJECTED' | 'SUBMITTED' | 'CONFIRMED' | 'FAILED';
// Transaction details type
type Transaction = {
id: string;
chainId: string;
from?: string;
to: string;
value: string;
data?: string;
gasLimit?: string;
status: TransactionStatus;
txHash?: string;
createdAt: string;
updatedAt: string;
};
// Chain information type
type Chain = {
id: string;
name: string;
currency: string;
rpcUrl: string;
explorerUrl: string;
};
const TransactionReview: React.FC = () => {
const { txId } = useParams<{ txId: string }>();
const navigate = useNavigate();
const { account, connect, signTransaction, chainId, switchChain } = useWallet();
const [transaction, setTransaction] = useState<Transaction | null>(null);
const [chain, setChain] = useState<Chain | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState<boolean>(false);
const [success, setSuccess] = useState<boolean>(false);
// Fetch transaction details
useEffect(() => {
const fetchTransaction = async () => {
setLoading(true);
try {
if (!txId) {
throw new Error('Transaction ID is missing');
}
const tx = await getTransaction(txId);
setTransaction(tx);
// Fetch chain details
const chainInfo = await getChainById(tx.chainId);
setChain(chainInfo);
} catch (err) {
setError('Failed to load transaction: ' + (err instanceof Error ? err.message : String(err)));
} finally {
setLoading(false);
}
};
fetchTransaction();
}, [txId]);
// Handle wallet connection
const handleConnect = async () => {
try {
await connect();
} catch (err) {
setError('Failed to connect wallet: ' + (err instanceof Error ? err.message : String(err)));
}
};
// Handle chain switching
const handleSwitchChain = async () => {
if (!chain) return;
try {
await switchChain(chain.id);
} catch (err) {
setError('Failed to switch network: ' + (err instanceof Error ? err.message : String(err)));
}
};
// Handle transaction approval
const handleApprove = async () => {
if (!transaction || !account || !chain) return;
setSubmitting(true);
setError(null);
try {
// Ensure user is connected to correct chain
if (chainId !== transaction.chainId) {
await switchChain(transaction.chainId);
}
// Create transaction object
const tx = {
to: transaction.to,
value: ethers.utils.parseEther(transaction.value),
data: transaction.data || '0x',
gasLimit: transaction.gasLimit ? ethers.BigNumber.from(transaction.gasLimit) : undefined,
chainId: parseInt(transaction.chainId)
};
// Sign transaction
const signedTx = await signTransaction(tx);
// Submit signed transaction
const result = await submitTransaction(transaction.id, signedTx);
setSuccess(true);
setTimeout(() => {
navigate(`/tx/${transaction.id}/success`);
}, 2000);
} catch (err) {
setError('Transaction failed: ' + (err instanceof Error ? err.message : String(err)));
} finally {
setSubmitting(false);
}
};
// Handle transaction rejection
const handleReject = async () => {
// Implement rejection logic here
navigate('/');
};
// Display loading state
if (loading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" height="100vh">
<CircularProgress />
</Box>
);
}
// Display error state
if (error) {
return (
<Box p={4}>
<Alert severity="error">{error}</Alert>
<Button variant="outlined" color="primary" onClick={() => navigate('/')} sx={{ mt: 2 }}>
Return to Home
</Button>
</Box>
);
}
// Display transaction not found
if (!transaction || !chain) {
return (
<Box p={4}>
<Alert severity="warning">Transaction not found</Alert>
<Button variant="outlined" color="primary" onClick={() => navigate('/')} sx={{ mt: 2 }}>
Return to Home
</Button>
</Box>
);
}
// Check if transaction can be approved
const canApprove = transaction.status === 'PENDING' && account && chainId === transaction.chainId;
return (
<Box p={4} maxWidth={800} mx="auto">
<Card>
<CardHeader
title="Transaction Review"
subheader={`Transaction ID: ${transaction.id}`}
/>
<Divider />
<CardContent>
<Grid container spacing={3}>
<Grid item xs={12}>
<Typography variant="h6" gutterBottom>
Network: {chain.name}
</Typography>
{account ? (
chainId !== transaction.chainId ? (
<Alert severity="warning" action={
<Button color="inherit" size="small" onClick={handleSwitchChain}>
Switch Network
</Button>
}>
Please switch to {chain.name} to approve this transaction
</Alert>
) : (
<Alert severity="success">Connected: {account}</Alert>
)
) : (
<Alert severity="info" action={
<Button color="inherit" size="small" onClick={handleConnect}>
Connect
</Button>
}>
Please connect your wallet to review and sign this transaction
</Alert>
)}
</Grid>
<Grid item xs={12}>
<Typography variant="h6" gutterBottom>
Transaction Details
</Typography>
<TableContainer component={Paper}>
<Table>
<TableBody>
<TableRow>
<TableCell component="th" scope="row">Status</TableCell>
<TableCell>
<Typography color={
transaction.status === 'CONFIRMED' ? 'success.main' :
transaction.status === 'FAILED' ? 'error.main' :
transaction.status === 'PENDING' ? 'warning.main' :
'text.primary'
}>
{transaction.status}
</Typography>
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">To</TableCell>
<TableCell>{transaction.to}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">Amount</TableCell>
<TableCell>{transaction.value} {chain.currency}</TableCell>
</TableRow>
{transaction.gasLimit && (
<TableRow>
<TableCell component="th" scope="row">Gas Limit</TableCell>
<TableCell>{transaction.gasLimit}</TableCell>
</TableRow>
)}
{transaction.data && transaction.data !== '0x' && (
<TableRow>
<TableCell component="th" scope="row">Data</TableCell>
<TableCell sx={{ wordBreak: 'break-all' }}>{transaction.data}</TableCell>
</TableRow>
)}
{transaction.txHash && (
<TableRow>
<TableCell component="th" scope="row">Transaction Hash</TableCell>
<TableCell>
<Link
href={`${chain.explorerUrl}/tx/${transaction.txHash}`}
target="_blank"
rel="noopener noreferrer"
>
{transaction.txHash}
</Link>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</Grid>
{transaction.status === 'PENDING' && (
<Grid item xs={12} sx={{ mt: 2 }}>
<Box display="flex" justifyContent="space-between">
<Button
variant="outlined"
color="error"
onClick={handleReject}
disabled={submitting}
>
Reject
</Button>
<Button
variant="contained"
color="primary"
onClick={handleApprove}
disabled={!canApprove || submitting}
>
{submitting ? <CircularProgress size={24} /> : 'Approve & Sign'}
</Button>
</Box>
</Grid>
)}
{success && (
<Grid item xs={12}>
<Alert severity="success">
Transaction successfully submitted! Redirecting...
</Alert>
</Grid>
)}
</Grid>
</CardContent>
</Card>
</Box>
);
};
export default TransactionReview;