139 lines
3.4 KiB
TypeScript
139 lines
3.4 KiB
TypeScript
/**
|
|
* Security utility functions for input validation and security checks
|
|
* Adapted for wagmi/viem
|
|
*/
|
|
|
|
import { isAddress, getAddress } from 'viem';
|
|
import type { PublicClient } from 'viem';
|
|
import { ERROR_MESSAGES, VALIDATION, SECURITY } from './constants';
|
|
|
|
/**
|
|
* Validates Ethereum address with checksum verification
|
|
*/
|
|
export function validateAddress(address: string): {
|
|
valid: boolean;
|
|
error?: string;
|
|
checksummed?: string;
|
|
} {
|
|
if (!address || typeof address !== 'string') {
|
|
return { valid: false, error: ERROR_MESSAGES.INVALID_ADDRESS };
|
|
}
|
|
|
|
if (address.length > VALIDATION.ADDRESS_MAX_LENGTH) {
|
|
return { valid: false, error: 'Address exceeds maximum length' };
|
|
}
|
|
|
|
if (!isAddress(address)) {
|
|
return { valid: false, error: 'Invalid Ethereum address format' };
|
|
}
|
|
|
|
try {
|
|
const checksummed = getAddress(address);
|
|
return { valid: true, checksummed };
|
|
} catch (error: unknown) {
|
|
return {
|
|
valid: false,
|
|
error: error instanceof Error ? error.message : 'Address validation failed',
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if address is a contract (has code)
|
|
*/
|
|
export async function isContractAddress(
|
|
address: string,
|
|
publicClient: Pick<PublicClient, 'getBytecode'> // viem PublicClient
|
|
): Promise<boolean> {
|
|
try {
|
|
const code = await publicClient.getBytecode({ address: address as `0x${string}` });
|
|
return code !== undefined && code !== '0x' && code !== '0x0';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates transaction data field
|
|
*/
|
|
export function validateTransactionData(data: string): {
|
|
valid: boolean;
|
|
error?: string;
|
|
} {
|
|
if (!data) {
|
|
return { valid: true }; // Empty data is valid
|
|
}
|
|
|
|
if (typeof data !== 'string') {
|
|
return { valid: false, error: 'Data must be a string' };
|
|
}
|
|
|
|
if (!data.startsWith('0x')) {
|
|
return { valid: false, error: 'Data must start with 0x' };
|
|
}
|
|
|
|
if (data.length > SECURITY.MAX_TRANSACTION_DATA_LENGTH * 2 + 2) {
|
|
return {
|
|
valid: false,
|
|
error: `Data exceeds maximum length (${SECURITY.MAX_TRANSACTION_DATA_LENGTH} bytes)`,
|
|
};
|
|
}
|
|
|
|
if (!/^0x[0-9a-fA-F]*$/.test(data)) {
|
|
return { valid: false, error: 'Data contains invalid hex characters' };
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
/**
|
|
* Rate limiter for admin functions
|
|
*/
|
|
export class RateLimiter {
|
|
private requests: Map<string, number[]> = new Map();
|
|
|
|
checkLimit(identifier: string, maxRequests: number = SECURITY.DEFAULT_RATE_LIMIT_REQUESTS): boolean {
|
|
const now = Date.now();
|
|
const windowStart = now - SECURITY.DEFAULT_RATE_LIMIT_WINDOW_MS;
|
|
|
|
if (!this.requests.has(identifier)) {
|
|
this.requests.set(identifier, []);
|
|
}
|
|
|
|
const requests = this.requests.get(identifier)!;
|
|
const recentRequests = requests.filter((time) => time > windowStart);
|
|
|
|
if (recentRequests.length >= maxRequests) {
|
|
return false;
|
|
}
|
|
|
|
recentRequests.push(now);
|
|
this.requests.set(identifier, recentRequests);
|
|
return true;
|
|
}
|
|
|
|
clear(identifier?: string): void {
|
|
if (identifier) {
|
|
this.requests.delete(identifier);
|
|
} else {
|
|
this.requests.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate secure ID
|
|
*/
|
|
export function generateSecureId(): string {
|
|
return `tx_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
}
|
|
|
|
/**
|
|
* Validate network ID
|
|
*/
|
|
export function validateNetworkId(networkId: number): boolean {
|
|
// Add supported networks as needed
|
|
const supportedNetworks = [1, 5, 137, 42161, 10, 8453, 100, 56, 250, 43114, 138];
|
|
return supportedNetworks.includes(networkId);
|
|
}
|