import { ThreatClient, Chain } from '@webacy-xyz/sdk';
const client = new ThreatClient({
apiKey: process.env.WEBACY_API_KEY,
defaultChain: Chain.ETH,
});
type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
interface PreSigningResult {
allowed: boolean;
riskLevel: RiskLevel;
assetChanges?: Array<{
type: string;
symbol: string;
amount: string;
}>;
warnings: string[];
blockReason?: string;
}
// Main pre-signing check
async function preSigningCheck(
userAddress: string,
request: TransactionRequest | EIP712Request,
chain: number,
dappDomain?: string
): Promise<PreSigningResult> {
// Determine request type
if (isEIP712Request(request)) {
return checkEIP712Signature(userAddress, request, dappDomain);
} else {
return checkTransaction(userAddress, request, chain, dappDomain);
}
}
// EIP-712 signature check
async function checkEIP712Signature(
userAddress: string,
request: EIP712Request,
dappDomain?: string
): Promise<PreSigningResult> {
const result = await client.scan.scanEip712(userAddress, {
msg: {
from: userAddress,
data: request.typedData,
},
domain: dappDomain,
});
const warnings: string[] = [];
// Check for permit signatures
if (result.messageType?.toLowerCase().includes('permit')) {
warnings.push('This is a PERMIT signature - it can authorize token spending');
// Check for unlimited approval
const message = request.typedData.message;
if (message.value === '115792089237316195423570985008687907853269984665640564039457584007913129639935') {
warnings.push('UNLIMITED token approval requested');
}
}
// Block critical risk
if (result.riskLevel === 'critical') {
return {
allowed: false,
riskLevel: 'critical',
warnings,
blockReason: 'This signature matches known malicious patterns and could drain your wallet.',
};
}
return {
allowed: true,
riskLevel: result.riskLevel as RiskLevel,
warnings,
};
}
// Transaction check
async function checkTransaction(
userAddress: string,
request: TransactionRequest,
chain: number,
dappDomain?: string
): Promise<PreSigningResult> {
const warnings: string[] = [];
// Check recipient for poisoning if present
if (request.to) {
const poisoning = await client.addresses.checkPoisoning(request.to, {
chain: chainIdToChain(chain),
});
if (poisoning.is_poisoned) {
return {
allowed: false,
riskLevel: 'critical',
warnings: ['Address poisoning attack detected'],
blockReason: 'This address appears to be part of an address poisoning scam.',
};
}
}
// Simulate transaction
const simulation = await client.scan.scanTransaction(userAddress, {
tx: {
from: userAddress,
to: request.to,
data: request.data,
value: request.value || '0x0',
},
chain,
domain: dappDomain,
});
// Collect warnings
warnings.push(...(simulation.warnings || []));
// Check for dangerous approvals in asset changes
const approvals = simulation.assetChanges?.filter(c => c.type === 'approval') || [];
for (const approval of approvals) {
if (approval.amount === 'unlimited') {
warnings.push(`Unlimited approval to ${approval.spender}`);
}
}
// Block critical transactions
if (simulation.riskLevel === 'critical') {
return {
allowed: false,
riskLevel: 'critical',
assetChanges: simulation.assetChanges,
warnings,
blockReason: 'This transaction has been identified as dangerous.',
};
}
return {
allowed: true,
riskLevel: simulation.riskLevel as RiskLevel,
assetChanges: simulation.assetChanges,
warnings,
};
}
// Approval health check
async function getApprovalHealth(
userAddress: string,
chain: Chain
): Promise<{
dangerous: number;
unlimited: number;
total: number;
needsAttention: boolean;
approvals: ApprovalInfo[];
}> {
const result = await client.wallets.getApprovals(userAddress, { chain });
const categorized = result.approvals.map(a => ({
...a,
category: categorizeApproval(a),
}));
const dangerous = categorized.filter(a => a.category === 'dangerous').length;
const unlimited = categorized.filter(a => a.category === 'unlimited').length;
return {
dangerous,
unlimited,
total: result.approvals.length,
needsAttention: dangerous > 0,
approvals: categorized,
};
}
function categorizeApproval(approval: Approval): 'dangerous' | 'unlimited' | 'safe' {
if (approval.spenderRisk === 'high' || !approval.spenderVerified) {
return 'dangerous';
}
if (approval.isUnlimited) {
return 'unlimited';
}
return 'safe';
}
// Helper to convert chain ID to Chain enum
function chainIdToChain(chainId: number): Chain {
const mapping: Record<number, Chain> = {
1: Chain.ETH,
56: Chain.BSC,
137: Chain.POL,
10: Chain.OPT,
42161: Chain.ARB,
8453: Chain.BASE,
};
return mapping[chainId] || Chain.ETH;
}
// Type guards
function isEIP712Request(request: any): request is EIP712Request {
return 'typedData' in request;
}
interface TransactionRequest {
to?: string;
data?: string;
value?: string;
}
interface EIP712Request {
typedData: {
types: Record<string, Array<{ name: string; type: string }>>;
primaryType: string;
domain: Record<string, any>;
message: Record<string, any>;
};
}