feat: comprehensive project improvements and fixes

- Fix all TypeScript compilation errors (40+ fixes)
  - Add missing type definitions (TransactionRequest, SafeInfo)
  - Fix TransactionRequestStatus vs TransactionStatus confusion
  - Fix import paths and provider type issues
  - Fix test file errors and mock providers

- Implement comprehensive security features
  - AES-GCM encryption with PBKDF2 key derivation
  - Input validation and sanitization
  - Rate limiting and nonce management
  - Replay attack prevention
  - Access control and authorization

- Add comprehensive test suite
  - Integration tests for transaction flow
  - Security validation tests
  - Wallet management tests
  - Encryption and rate limiter tests
  - E2E tests with Playwright

- Add extensive documentation
  - 12 numbered guides (setup, development, API, security, etc.)
  - Security documentation and audit reports
  - Code review and testing reports
  - Project organization documentation

- Update dependencies
  - Update axios to latest version (security fix)
  - Update React types to v18
  - Fix peer dependency warnings

- Add development tooling
  - CI/CD workflows (GitHub Actions)
  - Pre-commit hooks (Husky)
  - Linting and formatting (Prettier, ESLint)
  - Security audit workflow
  - Performance benchmarking

- Reorganize project structure
  - Move reports to docs/reports/
  - Clean up root directory
  - Organize documentation

- Add new features
  - Smart wallet management (Gnosis Safe, ERC4337)
  - Transaction execution and approval workflows
  - Balance management and token support
  - Error boundary and monitoring (Sentry)

- Fix WalletConnect configuration
  - Handle missing projectId gracefully
  - Add environment variable template
This commit is contained in:
defiQUG
2026-01-14 02:17:26 -08:00
parent cdde90c128
commit 55fe7d10eb
107 changed files with 25987 additions and 866 deletions

View File

@@ -0,0 +1,245 @@
"use client";
import React from "react";
import {
Box,
VStack,
HStack,
Text,
Heading,
Button,
useDisclosure,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
ModalFooter,
Badge,
Progress,
Table,
Thead,
Tr,
Th,
Tbody,
Td,
Code,
} from "@chakra-ui/react";
import { useTransaction } from "../../contexts/TransactionContext";
import { TransactionRequestStatus } from "../../types";
import { formatEther } from "ethers/lib/utils";
export default function TransactionApproval() {
const { pendingTransactions, approveTransaction, rejectTransaction, executeTransaction } =
useTransaction();
const { isOpen, onOpen, onClose } = useDisclosure();
const [selectedTx, setSelectedTx] = React.useState<string | null>(null);
const selectedTransaction = pendingTransactions.find((ptx) => ptx.id === selectedTx);
const handleApprove = async () => {
if (!selectedTx) return;
// Get approver address from active wallet or use a placeholder
// In production, this would get from the connected wallet
const approver = typeof window !== "undefined" && (window as any).ethereum
? await (window as any).ethereum.request({ method: "eth_accounts" }).then((accounts: string[]) => accounts[0])
: "0x0000000000000000000000000000000000000000";
await approveTransaction(selectedTx, approver || "0x0000000000000000000000000000000000000000");
onClose();
};
const handleReject = async () => {
if (!selectedTx) return;
const approver = typeof window !== "undefined" && (window as any).ethereum
? await (window as any).ethereum.request({ method: "eth_accounts" }).then((accounts: string[]) => accounts[0])
: "0x0000000000000000000000000000000000000000";
await rejectTransaction(selectedTx, approver || "0x0000000000000000000000000000000000000000");
onClose();
};
const handleExecute = async () => {
if (!selectedTx) return;
const hash = await executeTransaction(selectedTx);
if (hash) {
// Transaction executed successfully
}
onClose();
};
return (
<Box>
<Heading size="md" mb={4}>
Pending Transactions
</Heading>
{pendingTransactions.length === 0 ? (
<Box p={4} textAlign="center" color="gray.400">
<Text>No pending transactions</Text>
</Box>
) : (
<Table variant="simple" size="sm">
<Thead>
<Tr>
<Th>ID</Th>
<Th>To</Th>
<Th>Value</Th>
<Th>Approvals</Th>
<Th>Status</Th>
<Th>Actions</Th>
</Tr>
</Thead>
<Tbody>
{pendingTransactions.map((ptx) => (
<Tr key={ptx.id}>
<Td>
<Text fontSize="xs">{ptx.id.slice(0, 10)}...</Text>
</Td>
<Td>
<Text fontSize="sm">{ptx.transaction.to.slice(0, 10)}...</Text>
</Td>
<Td>
<Text fontSize="sm">
{parseFloat(formatEther(ptx.transaction.value || "0")).toFixed(4)} ETH
</Text>
</Td>
<Td>
<Text fontSize="sm">
{ptx.approvalCount} / {ptx.requiredApprovals}
</Text>
<Progress
value={(ptx.approvalCount / ptx.requiredApprovals) * 100}
size="sm"
colorScheme="green"
mt={1}
/>
</Td>
<Td>
<Badge
colorScheme={
ptx.transaction.status === TransactionRequestStatus.APPROVED
? "green"
: "yellow"
}
>
{ptx.transaction.status}
</Badge>
</Td>
<Td>
<HStack>
<Button
size="xs"
onClick={() => {
setSelectedTx(ptx.id);
onOpen();
}}
>
View
</Button>
{ptx.canExecute && (
<Button
size="xs"
colorScheme="green"
onClick={() => handleExecute()}
>
Execute
</Button>
)}
</HStack>
</Td>
</Tr>
))}
</Tbody>
</Table>
)}
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>Transaction Details</ModalHeader>
<ModalCloseButton />
<ModalBody>
{selectedTransaction && (
<VStack align="stretch" spacing={4}>
<Box>
<Text fontSize="sm" color="gray.400">
Transaction ID
</Text>
<Code>{selectedTransaction.id}</Code>
</Box>
<Box>
<Text fontSize="sm" color="gray.400">
To
</Text>
<Text>{selectedTransaction.transaction.to}</Text>
</Box>
<Box>
<Text fontSize="sm" color="gray.400">
Value
</Text>
<Text>
{parseFloat(formatEther(selectedTransaction.transaction.value || "0")).toFixed(
6
)}{" "}
ETH
</Text>
</Box>
<Box>
<Text fontSize="sm" color="gray.400">
Data
</Text>
<Code fontSize="xs" p={2} display="block" whiteSpace="pre-wrap">
{selectedTransaction.transaction.data || "0x"}
</Code>
</Box>
<Box>
<Text fontSize="sm" color="gray.400">
Approvals
</Text>
<Text>
{selectedTransaction.approvalCount} / {selectedTransaction.requiredApprovals}
</Text>
<Progress
value={
(selectedTransaction.approvalCount /
selectedTransaction.requiredApprovals) *
100
}
size="sm"
colorScheme="green"
mt={2}
/>
</Box>
<Box>
<Text fontSize="sm" color="gray.400">
Execution Method
</Text>
<Badge>{selectedTransaction.transaction.method}</Badge>
</Box>
</VStack>
)}
</ModalBody>
<ModalFooter>
<HStack>
<Button variant="ghost" onClick={onClose}>
Close
</Button>
{selectedTransaction && !selectedTransaction.canExecute && (
<>
<Button colorScheme="red" onClick={handleReject}>
Reject
</Button>
<Button colorScheme="blue" onClick={handleApprove}>
Approve
</Button>
</>
)}
</HStack>
</ModalFooter>
</ModalContent>
</Modal>
</Box>
);
}

View File

@@ -0,0 +1,416 @@
"use client";
import {
Box,
VStack,
HStack,
Text,
Heading,
Button,
FormControl,
FormLabel,
Input,
Select,
useToast,
useDisclosure,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
ModalFooter,
NumberInput,
NumberInputField,
Code,
} from "@chakra-ui/react";
import { useState } from "react";
import { useTransaction } from "../../contexts/TransactionContext";
import { useSmartWallet } from "../../contexts/SmartWalletContext";
import { TransactionExecutionMethod } from "../../types";
import { validateAddress, validateTransactionData, validateTransactionValue, sanitizeInput } from "../../utils/security";
import { ethers } from "ethers";
const ERC20_TRANSFER_ABI = [
"function transfer(address to, uint256 amount) returns (bool)",
];
export default function TransactionBuilder() {
const { createTransaction, estimateGas, defaultExecutionMethod, setDefaultExecutionMethod } =
useTransaction();
const { activeWallet, balance } = useSmartWallet();
const { isOpen, onOpen, onClose } = useDisclosure();
const toast = useToast();
const [toAddress, setToAddress] = useState("");
const [value, setValue] = useState("");
const [data, setData] = useState("");
const [isTokenTransfer, setIsTokenTransfer] = useState(false);
const [tokenAddress, setTokenAddress] = useState("");
const [tokenAmount, setTokenAmount] = useState("");
const [gasEstimate, setGasEstimate] = useState<any>(null);
const [isEstimating, setIsEstimating] = useState(false);
const handleEstimateGas = async () => {
if (!activeWallet || !toAddress) {
toast({
title: "Missing Information",
description: "Please fill in required fields",
status: "error",
isClosable: true,
});
return;
}
// Validate to address
const toValidation = validateAddress(toAddress);
if (!toValidation.valid) {
toast({
title: "Invalid Address",
description: toValidation.error || "Invalid 'to' address",
status: "error",
isClosable: true,
});
return;
}
// Validate transaction data
if (data) {
const dataValidation = validateTransactionData(data);
if (!dataValidation.valid) {
toast({
title: "Invalid Data",
description: dataValidation.error || "Invalid transaction data",
status: "error",
isClosable: true,
});
return;
}
}
setIsEstimating(true);
try {
const valueHex = value
? ethers.utils.parseEther(value).toHexString()
: "0x0";
// Validate value
const valueValidation = validateTransactionValue(valueHex);
if (!valueValidation.valid) {
throw new Error(valueValidation.error || "Invalid transaction value");
}
const estimate = await estimateGas({
from: activeWallet.address,
to: toValidation.checksummed!,
value: valueHex,
data: data || "0x",
});
setGasEstimate(estimate);
} catch (error: any) {
toast({
title: "Estimation Failed",
description: error.message || "Failed to estimate gas",
status: "error",
isClosable: true,
});
} finally {
setIsEstimating(false);
}
};
const handleCreateTokenTransfer = () => {
if (!tokenAddress || !toAddress || !tokenAmount) {
toast({
title: "Missing Information",
description: "Please fill in all token transfer fields",
status: "error",
isClosable: true,
});
return;
}
// Find token info
const token = balance?.tokens.find(
(t) => t.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
);
if (!token) {
toast({
title: "Token Not Found",
description: "Token not found in balance. Please add it first.",
status: "error",
isClosable: true,
});
return;
}
// Encode transfer function
const iface = new ethers.utils.Interface(ERC20_TRANSFER_ABI);
const transferData = iface.encodeFunctionData("transfer", [
toAddress,
ethers.utils.parseUnits(tokenAmount, token.decimals),
]);
setData(transferData);
setValue("0");
setIsTokenTransfer(false);
toast({
title: "Transfer Data Generated",
description: "Token transfer data has been generated",
status: "success",
isClosable: true,
});
};
const handleCreateTransaction = async () => {
if (!activeWallet || !toAddress) {
toast({
title: "Missing Information",
description: "Please fill in required fields",
status: "error",
isClosable: true,
});
return;
}
// Validate all inputs
const toValidation = validateAddress(toAddress);
if (!toValidation.valid) {
toast({
title: "Invalid Address",
description: toValidation.error || "Invalid 'to' address",
status: "error",
isClosable: true,
});
return;
}
if (data) {
const dataValidation = validateTransactionData(data);
if (!dataValidation.valid) {
toast({
title: "Invalid Data",
description: dataValidation.error || "Invalid transaction data",
status: "error",
isClosable: true,
});
return;
}
}
try {
const valueHex = value
? ethers.utils.parseEther(value).toHexString()
: "0x0";
const valueValidation = validateTransactionValue(valueHex);
if (!valueValidation.valid) {
toast({
title: "Invalid Value",
description: valueValidation.error || "Invalid transaction value",
status: "error",
isClosable: true,
});
return;
}
// Validate gas estimate if provided
if (gasEstimate?.gasLimit) {
const { validateGasLimit } = await import("../../utils/security");
const gasValidation = validateGasLimit(gasEstimate.gasLimit);
if (!gasValidation.valid) {
toast({
title: "Invalid Gas Limit",
description: gasValidation.error || "Gas limit validation failed",
status: "error",
isClosable: true,
});
return;
}
}
const tx = await createTransaction({
from: activeWallet.address,
to: toValidation.checksummed!,
value: valueHex,
data: sanitizeInput(data || "0x"),
method: defaultExecutionMethod,
gasLimit: gasEstimate?.gasLimit,
gasPrice: gasEstimate?.gasPrice,
maxFeePerGas: gasEstimate?.maxFeePerGas,
maxPriorityFeePerGas: gasEstimate?.maxPriorityFeePerGas,
});
toast({
title: "Transaction Created",
description: `Transaction ${tx.id.slice(0, 10)}... created successfully`,
status: "success",
isClosable: true,
});
// Reset form
setToAddress("");
setValue("");
setData("");
setGasEstimate(null);
onClose();
} catch (error: any) {
toast({
title: "Failed",
description: error.message || "Failed to create transaction",
status: "error",
isClosable: true,
});
}
};
if (!activeWallet) {
return (
<Box p={4} borderWidth="1px" borderRadius="md">
<Text color="gray.400">No active wallet selected</Text>
</Box>
);
}
return (
<Box>
<HStack mb={4} justify="space-between">
<Heading size="md">Create Transaction</Heading>
<Button onClick={onOpen}>New Transaction</Button>
</HStack>
<Box mb={4} p={4} borderWidth="1px" borderRadius="md">
<FormControl>
<FormLabel>Default Execution Method</FormLabel>
<Select
value={defaultExecutionMethod}
onChange={(e) =>
setDefaultExecutionMethod(e.target.value as TransactionExecutionMethod)
}
>
<option value={TransactionExecutionMethod.DIRECT_ONCHAIN}>
Direct On-Chain
</option>
<option value={TransactionExecutionMethod.RELAYER}>Relayer</option>
<option value={TransactionExecutionMethod.SIMULATION}>Simulation</option>
</Select>
</FormControl>
</Box>
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>Create Transaction</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={4}>
<FormControl>
<FormLabel>To Address</FormLabel>
<Input
value={toAddress}
onChange={(e) => setToAddress(e.target.value)}
placeholder="0x..."
/>
</FormControl>
<FormControl>
<FormLabel>Native Value (ETH)</FormLabel>
<NumberInput
value={value}
onChange={(_, val) => setValue(val.toString())}
precision={18}
>
<NumberInputField />
</NumberInput>
</FormControl>
<Box w="full" p={4} borderWidth="1px" borderRadius="md">
<HStack mb={2}>
<Text fontWeight="bold">Token Transfer</Text>
<Button
size="sm"
onClick={() => setIsTokenTransfer(!isTokenTransfer)}
>
{isTokenTransfer ? "Cancel" : "Add Token Transfer"}
</Button>
</HStack>
{isTokenTransfer && (
<VStack spacing={3} align="stretch">
<FormControl>
<FormLabel>Token Address</FormLabel>
<Select
value={tokenAddress}
onChange={(e) => setTokenAddress(e.target.value)}
placeholder="Select token"
>
{balance?.tokens.map((token) => (
<option key={token.tokenAddress} value={token.tokenAddress}>
{token.symbol} - {token.name}
</option>
))}
</Select>
</FormControl>
<FormControl>
<FormLabel>Amount</FormLabel>
<Input
value={tokenAmount}
onChange={(e) => setTokenAmount(e.target.value)}
placeholder="0.0"
type="number"
/>
</FormControl>
<Button onClick={handleCreateTokenTransfer} colorScheme="blue">
Generate Transfer Data
</Button>
</VStack>
)}
</Box>
<FormControl>
<FormLabel>Data (Hex)</FormLabel>
<Code p={2} display="block" whiteSpace="pre-wrap" fontSize="xs">
<Input
value={data}
onChange={(e) => setData(e.target.value)}
placeholder="0x..."
fontFamily="mono"
/>
</Code>
</FormControl>
<HStack w="full">
<Button
onClick={handleEstimateGas}
isDisabled={isEstimating || !toAddress}
isLoading={isEstimating}
>
Estimate Gas
</Button>
{gasEstimate && (
<Text fontSize="sm" color="gray.400">
Gas: {gasEstimate.gasLimit} | Cost: ~
{ethers.utils.formatEther(gasEstimate.estimatedCost)} ETH
</Text>
)}
</HStack>
</VStack>
</ModalBody>
<ModalFooter>
<HStack>
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
<Button colorScheme="blue" onClick={handleCreateTransaction}>
Create Transaction
</Button>
</HStack>
</ModalFooter>
</ModalContent>
</Modal>
</Box>
);
}

View File

@@ -0,0 +1,256 @@
"use client";
import {
Box,
VStack,
HStack,
Text,
Heading,
Button,
Table,
Thead,
Tr,
Th,
Tbody,
Td,
Badge,
useDisclosure,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
Code,
Link,
Select,
} from "@chakra-ui/react";
import { useState } from "react";
import { useTransaction } from "../../contexts/TransactionContext";
import { TransactionRequestStatus, TransactionStatus, TransactionExecutionMethod } from "../../types";
import { utils } from "ethers";
const getStatusColor = (status: TransactionRequestStatus) => {
switch (status) {
case TransactionRequestStatus.SUCCESS:
return "green";
case TransactionRequestStatus.FAILED:
return "red";
case TransactionRequestStatus.EXECUTING:
return "blue";
case TransactionRequestStatus.APPROVED:
return "yellow";
case TransactionRequestStatus.REJECTED:
return "red";
default:
return "gray";
}
};
export default function TransactionHistory() {
const { transactions } = useTransaction();
const { isOpen, onOpen, onClose } = useDisclosure();
const [selectedTx, setSelectedTx] = useState<string | null>(null);
const [filter, setFilter] = useState<TransactionRequestStatus | "ALL">("ALL");
const selectedTransaction = transactions.find((tx) => tx.id === selectedTx);
const filteredTransactions = transactions.filter((tx) => {
if (filter === "ALL") return true;
return tx.status === filter;
});
const getExplorerUrl = (hash: string, networkId: number) => {
const explorers: Record<number, string> = {
1: `https://etherscan.io/tx/${hash}`,
5: `https://goerli.etherscan.io/tx/${hash}`,
137: `https://polygonscan.com/tx/${hash}`,
42161: `https://arbiscan.io/tx/${hash}`,
10: `https://optimistic.etherscan.io/tx/${hash}`,
8453: `https://basescan.org/tx/${hash}`,
};
return explorers[networkId] || `https://etherscan.io/tx/${hash}`;
};
return (
<Box>
<HStack mb={4} justify="space-between">
<Heading size="md">Transaction History</Heading>
<Select
value={filter}
onChange={(e) => setFilter(e.target.value as TransactionRequestStatus | "ALL")}
width="200px"
>
<option value="ALL">All Status</option>
<option value={TransactionRequestStatus.PENDING}>Pending</option>
<option value={TransactionRequestStatus.APPROVED}>Approved</option>
<option value={TransactionRequestStatus.SUCCESS}>Success</option>
<option value={TransactionRequestStatus.FAILED}>Failed</option>
<option value={TransactionRequestStatus.REJECTED}>Rejected</option>
</Select>
</HStack>
{filteredTransactions.length === 0 ? (
<Box p={4} textAlign="center" color="gray.400">
<Text>No transactions found</Text>
</Box>
) : (
<Table variant="simple" size="sm">
<Thead>
<Tr>
<Th>ID</Th>
<Th>To</Th>
<Th>Value</Th>
<Th>Method</Th>
<Th>Status</Th>
<Th>Hash</Th>
<Th>Actions</Th>
</Tr>
</Thead>
<Tbody>
{filteredTransactions.map((tx) => (
<Tr key={tx.id}>
<Td>
<Text fontSize="xs">{tx.id.slice(0, 10)}...</Text>
</Td>
<Td>
<Text fontSize="sm">{tx.to.slice(0, 10)}...</Text>
</Td>
<Td>
<Text fontSize="sm">
{parseFloat(utils.formatEther(tx.value || "0")).toFixed(4)} ETH
</Text>
</Td>
<Td>
<Badge>{tx.method}</Badge>
</Td>
<Td>
<Badge colorScheme={getStatusColor(tx.status)}>{tx.status}</Badge>
</Td>
<Td>
{tx.hash ? (
<Link
href={getExplorerUrl(tx.hash, 1)}
isExternal
fontSize="xs"
color="blue.400"
>
{tx.hash.slice(0, 10)}...
</Link>
) : (
<Text fontSize="xs" color="gray.400">
-
</Text>
)}
</Td>
<Td>
<Button
size="xs"
onClick={() => {
setSelectedTx(tx.id);
onOpen();
}}
>
View
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
)}
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>Transaction Details</ModalHeader>
<ModalCloseButton />
<ModalBody>
{selectedTransaction && (
<VStack align="stretch" spacing={4}>
<Box>
<Text fontSize="sm" color="gray.400">
Transaction ID
</Text>
<Code>{selectedTransaction.id}</Code>
</Box>
<Box>
<Text fontSize="sm" color="gray.400">
From
</Text>
<Text>{selectedTransaction.from}</Text>
</Box>
<Box>
<Text fontSize="sm" color="gray.400">
To
</Text>
<Text>{selectedTransaction.to}</Text>
</Box>
<Box>
<Text fontSize="sm" color="gray.400">
Value
</Text>
<Text>
{parseFloat(utils.formatEther(selectedTransaction.value || "0")).toFixed(6)} ETH
</Text>
</Box>
<Box>
<Text fontSize="sm" color="gray.400">
Data
</Text>
<Code fontSize="xs" p={2} display="block" whiteSpace="pre-wrap">
{selectedTransaction.data || "0x"}
</Code>
</Box>
<Box>
<Text fontSize="sm" color="gray.400">
Status
</Text>
<Badge colorScheme={getStatusColor(selectedTransaction.status)}>
{selectedTransaction.status}
</Badge>
</Box>
<Box>
<Text fontSize="sm" color="gray.400">
Execution Method
</Text>
<Badge>{selectedTransaction.method}</Badge>
</Box>
{selectedTransaction.hash && (
<Box>
<Text fontSize="sm" color="gray.400">
Transaction Hash
</Text>
<Link
href={getExplorerUrl(selectedTransaction.hash, 1)}
isExternal
color="blue.400"
>
{selectedTransaction.hash}
</Link>
</Box>
)}
{selectedTransaction.error && (
<Box>
<Text fontSize="sm" color="gray.400">
Error
</Text>
<Text color="red.400">{selectedTransaction.error}</Text>
</Box>
)}
{selectedTransaction.executedAt && (
<Box>
<Text fontSize="sm" color="gray.400">
Executed At
</Text>
<Text>{new Date(selectedTransaction.executedAt).toLocaleString()}</Text>
</Box>
)}
</VStack>
)}
</ModalBody>
</ModalContent>
</Modal>
</Box>
);
}