edith-skep3 2.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +615 -0
- package/assets/banner.png +0 -0
- package/dist/ai.js +153 -0
- package/dist/brain.js +482 -0
- package/dist/explorer.js +167 -0
- package/dist/index.js +550 -0
- package/dist/intel.js +35 -0
- package/dist/parser.js +229 -0
- package/dist/simulator.js +229 -0
- package/package.json +75 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { createPublicClient, http, formatEther, formatUnits } from 'viem';
|
|
2
|
+
import { mainnet } from 'viem/chains';
|
|
3
|
+
const LOCAL_ANVIL_RPC = 'http://127.0.0.1:8545';
|
|
4
|
+
// Known ERC-20 event signatures
|
|
5
|
+
const KNOWN_TOPICS = {
|
|
6
|
+
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef': 'Transfer(address,address,uint256)',
|
|
7
|
+
'0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925': 'Approval(address,address,uint256)',
|
|
8
|
+
'0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c': 'Deposit(address,uint256)',
|
|
9
|
+
'0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65': 'Withdrawal(address,uint256)',
|
|
10
|
+
};
|
|
11
|
+
// Suspicious patterns in traces
|
|
12
|
+
const SUSPICIOUS_OPCODES = ['DELEGATECALL', 'SELFDESTRUCT', 'CREATE2'];
|
|
13
|
+
import axios from 'axios';
|
|
14
|
+
export class TransactionParser {
|
|
15
|
+
client;
|
|
16
|
+
signatureCache = {};
|
|
17
|
+
constructor(rpcUrl = LOCAL_ANVIL_RPC) {
|
|
18
|
+
this.client = createPublicClient({
|
|
19
|
+
chain: mainnet,
|
|
20
|
+
transport: http(rpcUrl),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async parseReceipt(txHash) {
|
|
24
|
+
const receipt = await this.client.getTransactionReceipt({ hash: txHash });
|
|
25
|
+
const tx = await this.client.getTransaction({ hash: txHash });
|
|
26
|
+
const tokenLosses = [];
|
|
27
|
+
const logs = receipt.logs.map((log) => {
|
|
28
|
+
const topic0 = log.topics[0];
|
|
29
|
+
const eventName = topic0 ? (KNOWN_TOPICS[topic0] || `Unknown(${topic0.slice(0, 10)}...)`) : 'Unknown';
|
|
30
|
+
// Try to decode Transfer/Approval
|
|
31
|
+
let decoded;
|
|
32
|
+
if (topic0 === '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef') {
|
|
33
|
+
const from = '0x' + (log.topics[1] || '').slice(26);
|
|
34
|
+
const to = '0x' + (log.topics[2] || '').slice(26);
|
|
35
|
+
const amount = BigInt(log.data || '0x0');
|
|
36
|
+
decoded = { from, to, amount: amount.toString() };
|
|
37
|
+
// Track if the sender is losing ERC20 tokens
|
|
38
|
+
if (from.toLowerCase() === tx.from.toLowerCase()) {
|
|
39
|
+
let decimals = 18; // fallback
|
|
40
|
+
// A quick heuristic for USDC/USDT which have 6 decimals
|
|
41
|
+
if (log.address.toLowerCase() === '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' ||
|
|
42
|
+
log.address.toLowerCase() === '0xdac17f958d2ee523a2206206994597c13d831ec7') {
|
|
43
|
+
decimals = 6;
|
|
44
|
+
}
|
|
45
|
+
tokenLosses.push({
|
|
46
|
+
tokenAddress: log.address,
|
|
47
|
+
amount: amount.toString(),
|
|
48
|
+
formatted: formatUnits(amount, decimals)
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (topic0 === '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925') {
|
|
53
|
+
const owner = '0x' + (log.topics[1] || '').slice(26);
|
|
54
|
+
const spender = '0x' + (log.topics[2] || '').slice(26);
|
|
55
|
+
const amount = BigInt(log.data || '0x0');
|
|
56
|
+
const isInfinite = amount > BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
|
|
57
|
+
decoded = { owner, spender, amount: isInfinite ? 'INFINITE (Max Uint256)' : amount.toString() };
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
address: log.address,
|
|
61
|
+
eventName,
|
|
62
|
+
raw: log.data,
|
|
63
|
+
decoded,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
const warnings = this.detectWarningsFromLogs(logs, tx.from);
|
|
67
|
+
return {
|
|
68
|
+
hash: txHash,
|
|
69
|
+
to: receipt.to || 'Contract Creation',
|
|
70
|
+
from: receipt.from,
|
|
71
|
+
value: formatEther(tx.value) + ' ETH',
|
|
72
|
+
status: receipt.status === 'success' ? 'success' : 'reverted',
|
|
73
|
+
gasUsed: receipt.gasUsed.toString(),
|
|
74
|
+
gasFee: receipt.gasUsed * (receipt.effectiveGasPrice || BigInt(0)),
|
|
75
|
+
logs,
|
|
76
|
+
tokenLosses,
|
|
77
|
+
warnings,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async parseTrace(rawTrace) {
|
|
81
|
+
if (!rawTrace)
|
|
82
|
+
return null;
|
|
83
|
+
const suspiciousOps = [];
|
|
84
|
+
const extractSuspicious = async (traceNode) => {
|
|
85
|
+
const childPromises = (traceNode.calls || []).map((node) => extractSuspicious(node));
|
|
86
|
+
const calls = await Promise.all(childPromises);
|
|
87
|
+
// Check for suspicious opcodes
|
|
88
|
+
if (traceNode.type && SUSPICIOUS_OPCODES.includes(traceNode.type.toUpperCase())) {
|
|
89
|
+
suspiciousOps.push(`${traceNode.type} to ${traceNode.to}`);
|
|
90
|
+
}
|
|
91
|
+
let decodedInput = `Raw: ${(traceNode.input || '0x').slice(0, 512)}...`;
|
|
92
|
+
// Auto decoding using 4byte directory
|
|
93
|
+
if (traceNode.input && traceNode.input.length >= 10 && traceNode.input !== '0x') {
|
|
94
|
+
const selector = traceNode.input.slice(0, 10);
|
|
95
|
+
if (!this.signatureCache[selector]) {
|
|
96
|
+
try {
|
|
97
|
+
const res = await axios.get(`https://www.4byte.directory/api/v1/signatures/?hex_signature=${selector}`);
|
|
98
|
+
if (res.data && res.data.results && res.data.results.length > 0) {
|
|
99
|
+
this.signatureCache[selector] = res.data.results[0].text_signature;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
this.signatureCache[selector] = `Unknown(${selector})`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
this.signatureCache[selector] = `Unknown(${selector})`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
decodedInput = `${this.signatureCache[selector]} | ${decodedInput}`;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
type: traceNode.type || 'CALL',
|
|
113
|
+
to: traceNode.to || 'unknown',
|
|
114
|
+
from: traceNode.from || 'unknown',
|
|
115
|
+
input: decodedInput,
|
|
116
|
+
output: (traceNode.output || '0x').slice(0, 512),
|
|
117
|
+
calls: calls.length > 0 ? calls : undefined,
|
|
118
|
+
suspiciousOps,
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
return await extractSuspicious(rawTrace);
|
|
122
|
+
}
|
|
123
|
+
parseStateDiff(rawDiff, impersonatedAddress) {
|
|
124
|
+
if (!rawDiff)
|
|
125
|
+
return { stateChanges: [] };
|
|
126
|
+
const changes = [];
|
|
127
|
+
let balanceLoss;
|
|
128
|
+
const pre = rawDiff.pre || {};
|
|
129
|
+
const post = rawDiff.post || {};
|
|
130
|
+
const allAddresses = new Set([...Object.keys(pre), ...Object.keys(post)]);
|
|
131
|
+
allAddresses.forEach(address => {
|
|
132
|
+
const preState = pre[address] || { balance: '0x0', nonce: 0, storage: {} };
|
|
133
|
+
const postState = post[address] || { balance: '0x0', nonce: 0, storage: {} };
|
|
134
|
+
let balanceChange = undefined;
|
|
135
|
+
const preBal = BigInt(preState.balance || '0x0');
|
|
136
|
+
const postBal = BigInt(postState.balance || '0x0');
|
|
137
|
+
if (preBal !== postBal) {
|
|
138
|
+
const diff = postBal - preBal;
|
|
139
|
+
const sign = diff > 0n ? '+' : '-';
|
|
140
|
+
const absDiff = diff > 0n ? diff : -diff;
|
|
141
|
+
balanceChange = `${sign}${formatEther(absDiff)} ETH`;
|
|
142
|
+
if (address.toLowerCase() === impersonatedAddress.toLowerCase() && diff < 0n) {
|
|
143
|
+
balanceLoss = { amount: -diff, formatted: formatEther(-diff) };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const storageModified = JSON.stringify(preState.storage || {}) !== JSON.stringify(postState.storage || {});
|
|
147
|
+
const nonceChange = preState.nonce !== postState.nonce;
|
|
148
|
+
if (balanceChange || storageModified || nonceChange) {
|
|
149
|
+
changes.push({
|
|
150
|
+
address,
|
|
151
|
+
balanceChange,
|
|
152
|
+
nonceChange,
|
|
153
|
+
storageModified
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
return { stateChanges: changes, balanceLoss };
|
|
158
|
+
}
|
|
159
|
+
detectWarningsFromLogs(logs, senderAddress) {
|
|
160
|
+
const warnings = [];
|
|
161
|
+
for (const log of logs) {
|
|
162
|
+
// Infinite approval detection
|
|
163
|
+
if (log.eventName.startsWith('Approval') && log.decoded?.amount === 'INFINITE (Max Uint256)') {
|
|
164
|
+
warnings.push(`⚠️ INFINITE APPROVAL granted to ${log.decoded.spender} for token ${log.address}`);
|
|
165
|
+
}
|
|
166
|
+
// Transfer to unknown address (not sender)
|
|
167
|
+
if (log.eventName.startsWith('Transfer') && log.decoded) {
|
|
168
|
+
if (log.decoded.from.toLowerCase() === senderAddress.toLowerCase()) {
|
|
169
|
+
warnings.push(`⚠️ Token transfer FROM your wallet at ${log.address}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return warnings;
|
|
174
|
+
}
|
|
175
|
+
formatForAI(parsed, trace, contractCode) {
|
|
176
|
+
const logSummary = (parsed.logs || []).map(l => {
|
|
177
|
+
let str = ` - Event: ${l.eventName} on ${l.address}`;
|
|
178
|
+
if (l.decoded) {
|
|
179
|
+
str += `\n Decoded: ${JSON.stringify(l.decoded)}`;
|
|
180
|
+
}
|
|
181
|
+
return str;
|
|
182
|
+
}).join('\n');
|
|
183
|
+
const traceSummary = trace ? JSON.stringify({
|
|
184
|
+
type: trace.type,
|
|
185
|
+
to: trace.to,
|
|
186
|
+
suspiciousOps: trace.suspiciousOps,
|
|
187
|
+
numSubCalls: (trace.calls || []).length,
|
|
188
|
+
}, null, 2) : 'No trace available.';
|
|
189
|
+
let codeSection = '';
|
|
190
|
+
if (contractCode) {
|
|
191
|
+
// Include a safe snippet of the code (AI has token limits)
|
|
192
|
+
const cleanCode = contractCode.slice(0, 25000);
|
|
193
|
+
codeSection = `\n=== TARGET CONTRACT CODE (Logic) ===\n${cleanCode}\n`;
|
|
194
|
+
}
|
|
195
|
+
const stateDiffSummary = (parsed.stateChanges || []).map(sc => {
|
|
196
|
+
let s = ` - Account: ${sc.address}`;
|
|
197
|
+
if (sc.balanceChange)
|
|
198
|
+
s += ` | Balance: ${sc.balanceChange}`;
|
|
199
|
+
if (sc.nonceChange)
|
|
200
|
+
s += ` | Nonce changed`;
|
|
201
|
+
if (sc.storageModified)
|
|
202
|
+
s += ` | Storage modified`;
|
|
203
|
+
return s;
|
|
204
|
+
}).join('\n');
|
|
205
|
+
return `
|
|
206
|
+
=== TRANSACTION SIMULATION REPORT ===
|
|
207
|
+
Hash: ${parsed.hash}
|
|
208
|
+
From: ${parsed.from}
|
|
209
|
+
To: ${parsed.to}
|
|
210
|
+
Value: ${parsed.value}
|
|
211
|
+
Status: ${parsed.status?.toUpperCase()}
|
|
212
|
+
Gas Used: ${parsed.gasUsed}
|
|
213
|
+
Native Value Lost: ${parsed.balanceLoss ? parsed.balanceLoss.formatted + ' ETH' : '0 ETH'}
|
|
214
|
+
ERC20 Tokens Lost: ${(parsed.tokenLosses && parsed.tokenLosses.length > 0) ? parsed.tokenLosses.map(t => `${t.formatted} unit(s) of ${t.tokenAddress}`).join(', ') : 'None detected'}
|
|
215
|
+
${codeSection}
|
|
216
|
+
=== EMITTED EVENTS (Logs) ===
|
|
217
|
+
${logSummary || 'No events emitted.'}
|
|
218
|
+
|
|
219
|
+
=== STATE DIFFERENCES (Pre/Post) ===
|
|
220
|
+
${stateDiffSummary || 'No state changes.'}
|
|
221
|
+
|
|
222
|
+
=== CALL TRACE (Local Anvil) ===
|
|
223
|
+
${traceSummary}
|
|
224
|
+
|
|
225
|
+
=== PRE-DETECTED WARNINGS ===
|
|
226
|
+
${(parsed.warnings || []).join('\n') || 'None detected by parser.'}
|
|
227
|
+
`.trim();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { createPublicClient, http } from 'viem';
|
|
3
|
+
import { mainnet } from 'viem/chains';
|
|
4
|
+
// Public free RPC - no API key required, works for forking
|
|
5
|
+
export const DEFAULT_FORK_RPC = 'https://ethereum.publicnode.com';
|
|
6
|
+
const LOCAL_ANVIL_RPC = 'http://127.0.0.1:8545';
|
|
7
|
+
export class AnvilSimulator {
|
|
8
|
+
anvilProcess = null;
|
|
9
|
+
remoteRpcUrl;
|
|
10
|
+
ready = false;
|
|
11
|
+
constructor(remoteRpcUrl = DEFAULT_FORK_RPC) {
|
|
12
|
+
this.remoteRpcUrl = remoteRpcUrl;
|
|
13
|
+
}
|
|
14
|
+
async startFork(blockNumber) {
|
|
15
|
+
const args = [
|
|
16
|
+
'--fork-url', this.remoteRpcUrl,
|
|
17
|
+
'--port', '8545',
|
|
18
|
+
];
|
|
19
|
+
if (blockNumber) {
|
|
20
|
+
args.push('--fork-block-number', blockNumber.toString());
|
|
21
|
+
}
|
|
22
|
+
const anvilBin = `${process.env.HOME}/.foundry/bin/anvil`;
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
try {
|
|
25
|
+
this.anvilProcess = spawn(anvilBin, args, {
|
|
26
|
+
stdio: 'ignore', // suppress all anvil output
|
|
27
|
+
detached: false,
|
|
28
|
+
});
|
|
29
|
+
this.anvilProcess.on('error', (err) => {
|
|
30
|
+
reject(new Error(`Failed to start Anvil: ${err.message}. Run: curl -L https://foundry.paradigm.xyz | bash && foundryup`));
|
|
31
|
+
});
|
|
32
|
+
this.anvilProcess.on('exit', (code) => {
|
|
33
|
+
if (!this.ready && code !== null && code !== 0) {
|
|
34
|
+
reject(new Error(`Anvil crashed on startup (Exit ${code}). The target RPC might be rate-limiting or down.`));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
// Poll HTTP until Anvil is actually ready to accept connections
|
|
38
|
+
const poll = async () => {
|
|
39
|
+
const maxMs = 20_000;
|
|
40
|
+
const interval = 300;
|
|
41
|
+
let elapsed = 0;
|
|
42
|
+
while (elapsed < maxMs) {
|
|
43
|
+
if (this.ready)
|
|
44
|
+
return; // connected or rejected by exit
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch('http://127.0.0.1:8545', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] }),
|
|
50
|
+
signal: AbortSignal.timeout(500),
|
|
51
|
+
});
|
|
52
|
+
if (res.ok) {
|
|
53
|
+
this.ready = true;
|
|
54
|
+
resolve();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch { /* not ready yet */ }
|
|
59
|
+
await new Promise(r => setTimeout(r, interval));
|
|
60
|
+
elapsed += interval;
|
|
61
|
+
}
|
|
62
|
+
if (!this.ready)
|
|
63
|
+
reject(new Error('Anvil startup timed out after 20s. Check that port 8545 is free.'));
|
|
64
|
+
};
|
|
65
|
+
poll();
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
reject(new Error(`Anvil not found. Install Foundry: curl -L https://foundry.paradigm.xyz | bash`));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// Returns a viem client connected to the LOCAL Anvil node
|
|
73
|
+
getClient() {
|
|
74
|
+
return createPublicClient({
|
|
75
|
+
chain: mainnet,
|
|
76
|
+
transport: http(LOCAL_ANVIL_RPC),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Impersonate any address on the local fork (no private key needed)
|
|
80
|
+
async impersonateAccount(address) {
|
|
81
|
+
await fetch(LOCAL_ANVIL_RPC, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
jsonrpc: '2.0', id: 1,
|
|
86
|
+
method: 'anvil_impersonateAccount',
|
|
87
|
+
params: [address],
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async stopImpersonating(address) {
|
|
92
|
+
await fetch(LOCAL_ANVIL_RPC, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { 'Content-Type': 'application/json' },
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
jsonrpc: '2.0', id: 1,
|
|
97
|
+
method: 'anvil_stopImpersonatingAccount',
|
|
98
|
+
params: [address],
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// Fund an account on local fork (for gas)
|
|
103
|
+
async setBalance(address, ethAmount = 10n ** 18n) {
|
|
104
|
+
await fetch(LOCAL_ANVIL_RPC, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
jsonrpc: '2.0', id: 1,
|
|
109
|
+
method: 'anvil_setBalance',
|
|
110
|
+
params: [address, '0x' + ethAmount.toString(16)],
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// Send a raw transaction to local Anvil and mine it
|
|
115
|
+
async simulateTransaction(params) {
|
|
116
|
+
const txParams = {
|
|
117
|
+
from: params.from,
|
|
118
|
+
data: params.data || '0x',
|
|
119
|
+
value: params.value ? '0x' + params.value.toString(16) : '0x0',
|
|
120
|
+
gas: '0x' + (3_000_000).toString(16),
|
|
121
|
+
};
|
|
122
|
+
if (params.to)
|
|
123
|
+
txParams.to = params.to;
|
|
124
|
+
const sendRes = await fetch(LOCAL_ANVIL_RPC, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
jsonrpc: '2.0', id: 2,
|
|
129
|
+
method: 'eth_sendTransaction',
|
|
130
|
+
params: [txParams],
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
const sendData = await sendRes.json();
|
|
134
|
+
if (sendData.error)
|
|
135
|
+
throw new Error(`Simulation failed: ${sendData.error.message}`);
|
|
136
|
+
// Explicitly mine a block to include the tx
|
|
137
|
+
await fetch(LOCAL_ANVIL_RPC, {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: { 'Content-Type': 'application/json' },
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
jsonrpc: '2.0', id: 3,
|
|
142
|
+
method: 'evm_mine',
|
|
143
|
+
params: [],
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
const txHash = sendData.result;
|
|
147
|
+
// Poll until the receipt is available (Anvil indexes asynchronously)
|
|
148
|
+
for (let i = 0; i < 20; i++) {
|
|
149
|
+
const r = await fetch(LOCAL_ANVIL_RPC, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: { 'Content-Type': 'application/json' },
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
jsonrpc: '2.0', id: 10,
|
|
154
|
+
method: 'eth_getTransactionReceipt',
|
|
155
|
+
params: [txHash],
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
158
|
+
const rj = await r.json();
|
|
159
|
+
if (rj.result !== null)
|
|
160
|
+
break;
|
|
161
|
+
await new Promise(res => setTimeout(res, 200));
|
|
162
|
+
}
|
|
163
|
+
return txHash;
|
|
164
|
+
}
|
|
165
|
+
async getBalance(address) {
|
|
166
|
+
const res = await fetch(LOCAL_ANVIL_RPC, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: { 'Content-Type': 'application/json' },
|
|
169
|
+
body: JSON.stringify({
|
|
170
|
+
jsonrpc: '2.0', id: 7,
|
|
171
|
+
method: 'eth_getBalance',
|
|
172
|
+
params: [address, 'latest'],
|
|
173
|
+
}),
|
|
174
|
+
});
|
|
175
|
+
const data = await res.json();
|
|
176
|
+
return BigInt(data.result || '0x0');
|
|
177
|
+
}
|
|
178
|
+
async traceStateDiff(txHash) {
|
|
179
|
+
const res = await fetch(LOCAL_ANVIL_RPC, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: { 'Content-Type': 'application/json' },
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
jsonrpc: '2.0', id: 6,
|
|
184
|
+
method: 'debug_traceTransaction',
|
|
185
|
+
params: [txHash, { tracer: 'prestateTracer', tracerConfig: { diffMode: true } }],
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
const data = await res.json();
|
|
189
|
+
return data.result;
|
|
190
|
+
}
|
|
191
|
+
// Get the full EVM trace from LOCAL Anvil (this is free — it's your own node)
|
|
192
|
+
async traceTransaction(txHash) {
|
|
193
|
+
const res = await fetch(LOCAL_ANVIL_RPC, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: { 'Content-Type': 'application/json' },
|
|
196
|
+
body: JSON.stringify({
|
|
197
|
+
jsonrpc: '2.0', id: 4,
|
|
198
|
+
method: 'debug_traceTransaction',
|
|
199
|
+
params: [txHash, { tracer: 'callTracer' }],
|
|
200
|
+
}),
|
|
201
|
+
});
|
|
202
|
+
const data = await res.json();
|
|
203
|
+
if (data.error) {
|
|
204
|
+
// Fallback to structLogs tracer
|
|
205
|
+
const res2 = await fetch(LOCAL_ANVIL_RPC, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: { 'Content-Type': 'application/json' },
|
|
208
|
+
body: JSON.stringify({
|
|
209
|
+
jsonrpc: '2.0', id: 5,
|
|
210
|
+
method: 'debug_traceTransaction',
|
|
211
|
+
params: [txHash, {}],
|
|
212
|
+
}),
|
|
213
|
+
});
|
|
214
|
+
const data2 = await res2.json();
|
|
215
|
+
return data2.result;
|
|
216
|
+
}
|
|
217
|
+
return data.result;
|
|
218
|
+
}
|
|
219
|
+
async stop() {
|
|
220
|
+
if (this.anvilProcess) {
|
|
221
|
+
this.anvilProcess.kill('SIGTERM');
|
|
222
|
+
this.anvilProcess = null;
|
|
223
|
+
this.ready = false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
get isReady() {
|
|
227
|
+
return this.ready;
|
|
228
|
+
}
|
|
229
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "edith-skep3",
|
|
3
|
+
"version": "2.4.3",
|
|
4
|
+
"description": "Local-first Web3 wallet security CLI. Intercepts transactions, forks the EVM with Foundry Anvil, simulates execution, and runs on-device AI threat analysis via Ollama — all without sending a single byte to the cloud.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"edith": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"assets",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
18
|
+
"start": "node dist/index.js",
|
|
19
|
+
"dev": "npm run build && node dist/index.js",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"web3",
|
|
24
|
+
"ethereum",
|
|
25
|
+
"blockchain",
|
|
26
|
+
"security",
|
|
27
|
+
"wallet",
|
|
28
|
+
"transaction",
|
|
29
|
+
"simulation",
|
|
30
|
+
"evm",
|
|
31
|
+
"anvil",
|
|
32
|
+
"foundry",
|
|
33
|
+
"ollama",
|
|
34
|
+
"ai",
|
|
35
|
+
"drainer",
|
|
36
|
+
"honeypot",
|
|
37
|
+
"cli",
|
|
38
|
+
"proxy",
|
|
39
|
+
"skep3",
|
|
40
|
+
"edith"
|
|
41
|
+
],
|
|
42
|
+
"author": "Anubhav Singh <anubhav@anufied.me> (https://anufied.me)",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/anu-sin-theta/edith-skep3.git"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"provenance": true
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://skep3.anufied.pro",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/anu-sin-theta/edith-skep3/issues"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=18.0.0"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"axios": "^1.13.5",
|
|
60
|
+
"chalk": "^5.6.2",
|
|
61
|
+
"commander": "^14.0.3",
|
|
62
|
+
"inquirer": "^13.2.2",
|
|
63
|
+
"ollama": "^0.6.3",
|
|
64
|
+
"openai": "^6.22.0",
|
|
65
|
+
"ora": "^9.3.0",
|
|
66
|
+
"viem": "^2.45.3"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@types/inquirer": "^9.0.9",
|
|
70
|
+
"@types/node": "^25.2.3",
|
|
71
|
+
"@types/ora": "^3.1.0",
|
|
72
|
+
"ts-node": "^10.9.2",
|
|
73
|
+
"typescript": "^5.9.3"
|
|
74
|
+
}
|
|
75
|
+
}
|