- 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
251 lines
6.7 KiB
TypeScript
251 lines
6.7 KiB
TypeScript
import { providers, ethers } from "ethers";
|
|
import { TransactionRequest, TransactionExecutionMethod } from "../../types";
|
|
import { validateAddress, validateTransactionValue, validateGasLimit } from "../../utils/security";
|
|
import { SECURITY } from "../../utils/constants";
|
|
|
|
export async function executeDirectTransaction(
|
|
tx: TransactionRequest,
|
|
provider: providers.Provider,
|
|
signer: ethers.Signer
|
|
): Promise<string> {
|
|
// Validate addresses
|
|
if (!tx.to) {
|
|
throw new Error("Missing 'to' address");
|
|
}
|
|
|
|
const toValidation = validateAddress(tx.to);
|
|
if (!toValidation.valid) {
|
|
throw new Error(`Invalid 'to' address: ${toValidation.error}`);
|
|
}
|
|
|
|
// Validate value
|
|
if (tx.value) {
|
|
const valueValidation = validateTransactionValue(tx.value);
|
|
if (!valueValidation.valid) {
|
|
throw new Error(`Invalid transaction value: ${valueValidation.error}`);
|
|
}
|
|
}
|
|
|
|
// Validate gas limit if provided
|
|
if (tx.gasLimit) {
|
|
const gasValidation = validateGasLimit(tx.gasLimit);
|
|
if (!gasValidation.valid) {
|
|
throw new Error(`Invalid gas limit: ${gasValidation.error}`);
|
|
}
|
|
}
|
|
|
|
// Validate gas estimate if provided
|
|
if (tx.gasLimit) {
|
|
const MAX_GAS_LIMIT = ethers.BigNumber.from(SECURITY.MAX_GAS_LIMIT);
|
|
const gasLimitBN = ethers.BigNumber.from(tx.gasLimit);
|
|
if (gasLimitBN.gt(MAX_GAS_LIMIT)) {
|
|
throw new Error(`Gas limit ${gasLimitBN.toString()} exceeds maximum ${MAX_GAS_LIMIT.toString()}`);
|
|
}
|
|
}
|
|
|
|
const txParams: any = {
|
|
to: toValidation.checksummed!,
|
|
value: tx.value ? ethers.BigNumber.from(tx.value) : 0,
|
|
data: tx.data || "0x",
|
|
};
|
|
|
|
if (tx.gasLimit) {
|
|
txParams.gasLimit = ethers.BigNumber.from(tx.gasLimit);
|
|
}
|
|
|
|
if (tx.maxFeePerGas && tx.maxPriorityFeePerGas) {
|
|
txParams.maxFeePerGas = ethers.BigNumber.from(tx.maxFeePerGas);
|
|
txParams.maxPriorityFeePerGas = ethers.BigNumber.from(tx.maxPriorityFeePerGas);
|
|
} else if (tx.gasPrice) {
|
|
txParams.gasPrice = ethers.BigNumber.from(tx.gasPrice);
|
|
}
|
|
|
|
if (tx.nonce !== undefined) {
|
|
txParams.nonce = tx.nonce;
|
|
}
|
|
|
|
const transaction = await signer.sendTransaction(txParams);
|
|
return transaction.hash;
|
|
}
|
|
|
|
export async function executeRelayerTransaction(
|
|
tx: TransactionRequest,
|
|
relayerUrl: string,
|
|
apiKey?: string
|
|
): Promise<string> {
|
|
// Validate relayer URL
|
|
try {
|
|
const url = new URL(relayerUrl);
|
|
if (url.protocol !== "https:") {
|
|
throw new Error("Relayer URL must use HTTPS");
|
|
}
|
|
} catch {
|
|
throw new Error("Invalid relayer URL");
|
|
}
|
|
|
|
// Validate addresses
|
|
if (!tx.to) {
|
|
throw new Error("Missing 'to' address");
|
|
}
|
|
|
|
const toValidation = validateAddress(tx.to);
|
|
if (!toValidation.valid) {
|
|
throw new Error(`Invalid 'to' address: ${toValidation.error}`);
|
|
}
|
|
|
|
// Validate value
|
|
if (tx.value) {
|
|
const valueValidation = validateTransactionValue(tx.value);
|
|
if (!valueValidation.valid) {
|
|
throw new Error(`Invalid transaction value: ${valueValidation.error}`);
|
|
}
|
|
}
|
|
|
|
const payload: any = {
|
|
to: toValidation.checksummed!,
|
|
value: tx.value || "0",
|
|
data: tx.data || "0x",
|
|
};
|
|
|
|
if (tx.gasLimit) {
|
|
const gasValidation = validateGasLimit(tx.gasLimit);
|
|
if (!gasValidation.valid) {
|
|
throw new Error(`Invalid gas limit: ${gasValidation.error}`);
|
|
}
|
|
payload.gasLimit = tx.gasLimit;
|
|
}
|
|
|
|
if (tx.maxFeePerGas && tx.maxPriorityFeePerGas) {
|
|
payload.maxFeePerGas = tx.maxFeePerGas;
|
|
payload.maxPriorityFeePerGas = tx.maxPriorityFeePerGas;
|
|
} else if (tx.gasPrice) {
|
|
payload.gasPrice = tx.gasPrice;
|
|
}
|
|
|
|
const headers: Record<string, string> = {
|
|
"Content-Type": "application/json",
|
|
};
|
|
|
|
if (apiKey) {
|
|
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
}
|
|
|
|
// Add timeout to prevent hanging
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), SECURITY.RELAYER_REQUEST_TIMEOUT_MS);
|
|
|
|
try {
|
|
const response = await fetch(relayerUrl, {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify(payload),
|
|
signal: controller.signal,
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(`Relayer request failed: ${errorText || response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
const txHash = result.txHash || result.hash || result.transactionHash;
|
|
|
|
if (!txHash) {
|
|
throw new Error("Relayer did not return transaction hash");
|
|
}
|
|
|
|
return txHash;
|
|
} catch (error: any) {
|
|
clearTimeout(timeoutId);
|
|
if (error.name === "AbortError") {
|
|
throw new Error("Relayer request timeout");
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function simulateTransaction(
|
|
tx: TransactionRequest,
|
|
provider: providers.Provider,
|
|
from: string
|
|
): Promise<{ success: boolean; gasUsed: string; error?: string }> {
|
|
try {
|
|
// Validate addresses
|
|
const fromValidation = validateAddress(from);
|
|
if (!fromValidation.valid) {
|
|
return {
|
|
success: false,
|
|
gasUsed: "0",
|
|
error: `Invalid 'from' address: ${fromValidation.error}`,
|
|
};
|
|
}
|
|
|
|
if (!tx.to) {
|
|
return {
|
|
success: false,
|
|
gasUsed: "0",
|
|
error: "Missing 'to' address",
|
|
};
|
|
}
|
|
|
|
const toValidation = validateAddress(tx.to);
|
|
if (!toValidation.valid) {
|
|
return {
|
|
success: false,
|
|
gasUsed: "0",
|
|
error: `Invalid 'to' address: ${toValidation.error}`,
|
|
};
|
|
}
|
|
|
|
// Validate value
|
|
if (tx.value) {
|
|
const valueValidation = validateTransactionValue(tx.value);
|
|
if (!valueValidation.valid) {
|
|
return {
|
|
success: false,
|
|
gasUsed: "0",
|
|
error: `Invalid transaction value: ${valueValidation.error}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Add timeout to prevent hanging
|
|
const timeoutPromise = new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error("Gas estimation timeout")), SECURITY.GAS_ESTIMATION_TIMEOUT_MS)
|
|
);
|
|
|
|
const gasEstimate = await Promise.race([
|
|
provider.estimateGas({
|
|
from: fromValidation.checksummed!,
|
|
to: toValidation.checksummed!,
|
|
value: tx.value ? ethers.BigNumber.from(tx.value) : undefined,
|
|
data: tx.data || "0x",
|
|
}),
|
|
timeoutPromise,
|
|
]) as ethers.BigNumber;
|
|
|
|
// Validate gas estimate
|
|
const MAX_GAS_LIMIT = ethers.BigNumber.from(SECURITY.MAX_GAS_LIMIT);
|
|
if (gasEstimate.gt(MAX_GAS_LIMIT)) {
|
|
return {
|
|
success: false,
|
|
gasUsed: "0",
|
|
error: `Gas estimate ${gasEstimate.toString()} exceeds maximum ${MAX_GAS_LIMIT.toString()}`,
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
gasUsed: gasEstimate.toString(),
|
|
};
|
|
} catch (error: any) {
|
|
return {
|
|
success: false,
|
|
gasUsed: "0",
|
|
error: error.message || "Simulation failed",
|
|
};
|
|
}
|
|
}
|