ethnotary 1.0.0

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.
@@ -0,0 +1,169 @@
1
+ const { parentPort, workerData } = require('worker_threads');
2
+ const { ethers } = require('ethers');
3
+
4
+ async function scanNetwork(networkConfig, contractAddress, includeHistorical) {
5
+ const { networkKey, network, erc20ABI, erc721ABI } = networkConfig;
6
+
7
+ try {
8
+ const provider = new ethers.JsonRpcProvider(network.rpcUrl);
9
+ const discoveredTokens = [];
10
+ const tokenAddresses = new Set();
11
+
12
+ // Get current block
13
+ const currentBlock = await provider.getBlockNumber();
14
+ const fromBlock = includeHistorical ? 0 : Math.max(0, currentBlock - 900000);
15
+
16
+ // Transfer event topics
17
+ const transferTopic = ethers.id("Transfer(address,address,uint256)");
18
+
19
+ // Scan for transfers TO and FROM the contract
20
+ const [incomingLogs, outgoingLogs] = await Promise.all([
21
+ provider.getLogs({
22
+ fromBlock,
23
+ toBlock: currentBlock,
24
+ topics: [
25
+ transferTopic,
26
+ null,
27
+ ethers.zeroPadValue(contractAddress, 32)
28
+ ]
29
+ }),
30
+ provider.getLogs({
31
+ fromBlock,
32
+ toBlock: currentBlock,
33
+ topics: [
34
+ transferTopic,
35
+ ethers.zeroPadValue(contractAddress, 32),
36
+ null
37
+ ]
38
+ })
39
+ ]);
40
+
41
+ // Extract unique token addresses
42
+ [...incomingLogs, ...outgoingLogs].forEach(log => {
43
+ tokenAddresses.add(log.address.toLowerCase());
44
+ });
45
+
46
+ // Check each token contract
47
+ const tokenChecks = Array.from(tokenAddresses).map(async (tokenAddress) => {
48
+ try {
49
+ // Try ERC20
50
+ try {
51
+ const erc20Contract = new ethers.Contract(tokenAddress, erc20ABI, provider);
52
+ const [balance, decimals, symbol, name] = await Promise.all([
53
+ erc20Contract.balanceOf(contractAddress),
54
+ erc20Contract.decimals(),
55
+ erc20Contract.symbol(),
56
+ erc20Contract.name()
57
+ ]);
58
+
59
+ if (balance > 0n) {
60
+ const formattedBalance = ethers.formatUnits(balance, decimals);
61
+ return {
62
+ type: 'ERC20',
63
+ address: tokenAddress,
64
+ symbol: symbol,
65
+ name: name,
66
+ balance: formattedBalance,
67
+ rawBalance: balance.toString(),
68
+ decimals: decimals,
69
+ network: networkKey,
70
+ source: 'events'
71
+ };
72
+ }
73
+ } catch (erc20Error) {
74
+ // Try ERC721
75
+ try {
76
+ const erc721Contract = new ethers.Contract(tokenAddress, erc721ABI, provider);
77
+ const [balance, symbol, name] = await Promise.all([
78
+ erc721Contract.balanceOf(contractAddress),
79
+ erc721Contract.symbol(),
80
+ erc721Contract.name()
81
+ ]);
82
+
83
+ if (balance > 0n) {
84
+ // Try walletOfOwner first, fallback to tokenOfOwnerByIndex
85
+ let tokenIds = [];
86
+ try {
87
+ // Try walletOfOwner
88
+ const walletTokens = await erc721Contract.walletOfOwner(contractAddress);
89
+ tokenIds = walletTokens.map(id => id.toString());
90
+ console.log(`Found ${tokenIds.length} tokens using walletOfOwner`);
91
+ } catch (walletError) {
92
+ // Fallback to tokenOfOwnerByIndex
93
+ const promises = [];
94
+ for (let i = 0; i < Math.min(Number(balance), 50); i++) {
95
+ promises.push(erc721Contract.tokenOfOwnerByIndex(contractAddress, i)
96
+ .then(tokenId => tokenIds.push(tokenId.toString()))
97
+ .catch(() => {}));
98
+ }
99
+ await Promise.allSettled(promises);
100
+ console.log(`Found ${tokenIds.length} tokens using tokenOfOwnerByIndex`);
101
+ }
102
+
103
+ // Try to get token URIs
104
+ const tokenDetails = await Promise.all(
105
+ tokenIds.map(async (tokenId) => {
106
+ try {
107
+ const uri = await erc721Contract.tokenURI(tokenId);
108
+ return { id: tokenId, uri };
109
+ } catch (error) {
110
+ return { id: tokenId };
111
+ }
112
+ })
113
+ );
114
+
115
+ return {
116
+ type: 'ERC721',
117
+ address: tokenAddress,
118
+ symbol: symbol,
119
+ name: name,
120
+ balance: balance.toString(),
121
+ tokenIds: tokenIds,
122
+ tokenDetails: tokenDetails,
123
+ network: networkKey,
124
+ source: 'events'
125
+ };
126
+ }
127
+ } catch (erc721Error) {
128
+ // Not a valid token contract
129
+ }
130
+ }
131
+ } catch (error) {
132
+ // Log error but continue with other tokens
133
+ console.error(`Error checking token ${tokenAddress}: ${error.message}`);
134
+ }
135
+ return null;
136
+ });
137
+
138
+ const results = await Promise.allSettled(tokenChecks);
139
+ results.forEach(result => {
140
+ if (result.status === 'fulfilled' && result.value) {
141
+ discoveredTokens.push(result.value);
142
+ }
143
+ });
144
+
145
+ return {
146
+ networkKey,
147
+ networkName: network.name,
148
+ tokens: discoveredTokens
149
+ };
150
+
151
+ } catch (error) {
152
+ return {
153
+ networkKey,
154
+ networkName: network.name,
155
+ error: error.message,
156
+ tokens: []
157
+ };
158
+ }
159
+ }
160
+
161
+ // Handle messages from parent
162
+ parentPort.on('message', async ({ networkConfig, contractAddress, includeHistorical }) => {
163
+ try {
164
+ const result = await scanNetwork(networkConfig, contractAddress, includeHistorical);
165
+ parentPort.postMessage({ type: 'success', data: result });
166
+ } catch (error) {
167
+ parentPort.postMessage({ type: 'error', error: error.message });
168
+ }
169
+ });
@@ -0,0 +1,54 @@
1
+ {
2
+ "0xe4F717Fbe2901efF97d3fD48593Ef2c6453b4Eee_scan": {
3
+ "data": [
4
+ {
5
+ "type": "ERC721",
6
+ "address": "0xe29f8038d1a3445ab22ad1373c65ec0a6e1161a4",
7
+ "symbol": "BAYC",
8
+ "name": "BoredApeYachtClub",
9
+ "balance": "1",
10
+ "tokenIds": [
11
+ "340"
12
+ ],
13
+ "tokenDetails": [
14
+ {
15
+ "id": "340",
16
+ "uri": "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/340"
17
+ }
18
+ ],
19
+ "network": "sepolia",
20
+ "source": "events"
21
+ },
22
+ {
23
+ "type": "ERC20",
24
+ "address": "0x399ab04277063386023c41d0f77d15b79cb2d3be",
25
+ "symbol": "MT",
26
+ "name": "MyToken",
27
+ "balance": "10000.0",
28
+ "rawBalance": "10000000000000000000000",
29
+ "decimals": "18",
30
+ "network": "sepolia",
31
+ "source": "events"
32
+ },
33
+ {
34
+ "type": "ERC721",
35
+ "address": "0xe6c1ee6624c6819262f29199df25a70b2648b818",
36
+ "symbol": "DOODLE",
37
+ "name": "Doodles",
38
+ "balance": "1",
39
+ "tokenIds": [
40
+ "233"
41
+ ],
42
+ "tokenDetails": [
43
+ {
44
+ "id": "233",
45
+ "uri": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/233"
46
+ }
47
+ ],
48
+ "network": "sepolia",
49
+ "source": "events"
50
+ }
51
+ ],
52
+ "timestamp": 1761750128412
53
+ }
54
+ }
@@ -0,0 +1,92 @@
1
+ {
2
+ "metadata": {
3
+ "version": "1.0.0",
4
+ "description": "Token database for MultiSig dashboard",
5
+ "lastUpdated": "2025-10-27",
6
+ "totalTokens": 0,
7
+ "networks": ["sepolia", "base-sepolia", "arbitrum-sepolia"]
8
+ },
9
+ "networks": {
10
+ "sepolia": {
11
+ "chainId": 11155111,
12
+ "name": "Sepolia Testnet",
13
+ "tokens": {
14
+ "erc20": {
15
+ "testTokens": {
16
+ "WETH": {
17
+ "address": "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9",
18
+ "name": "Wrapped Ether",
19
+ "symbol": "WETH",
20
+ "decimals": 18,
21
+ "type": "wrapped"
22
+ },
23
+ "USDC": {
24
+ "address": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
25
+ "name": "USD Coin",
26
+ "symbol": "USDC",
27
+ "decimals": 6,
28
+ "type": "stablecoin"
29
+ },
30
+ "DAI": {
31
+ "address": "0x3e622317f8C93f7328350cF0B56d9eD4C620C5d6",
32
+ "name": "Dai Stablecoin",
33
+ "symbol": "DAI",
34
+ "decimals": 18,
35
+ "type": "stablecoin"
36
+ }
37
+ }
38
+ },
39
+ "erc721": {
40
+ "testNFTs": {
41
+ "MBAYC": {
42
+ "address": "0xE29F8038d1A3445Ab22AD1373c65eC0a6E1161a4",
43
+ "name": "Bored Ape Yacht Club",
44
+ "symbol": "MBAYC",
45
+ "type": "pfp"
46
+ }
47
+ }
48
+ }
49
+ }
50
+ },
51
+ "base-sepolia": {
52
+ "chainId": 84532,
53
+ "name": "Base Sepolia",
54
+ "tokens": {
55
+ "erc20": {
56
+ "testTokens": {
57
+ "WETH": {
58
+ "address": "0x4200000000000000000000000000000000000006",
59
+ "name": "Wrapped Ether",
60
+ "symbol": "WETH",
61
+ "decimals": 18,
62
+ "type": "wrapped"
63
+ }
64
+ }
65
+ },
66
+ "erc721": {
67
+ "testNFTs": {}
68
+ }
69
+ }
70
+ },
71
+ "arbitrum-sepolia": {
72
+ "chainId": 421614,
73
+ "name": "Arbitrum Sepolia",
74
+ "tokens": {
75
+ "erc20": {
76
+ "testTokens": {
77
+ "WETH": {
78
+ "address": "0x980B62Da83eFf3D4576C647993b0c1D7faf17c73",
79
+ "name": "Wrapped Ether",
80
+ "symbol": "WETH",
81
+ "decimals": 18,
82
+ "type": "wrapped"
83
+ }
84
+ }
85
+ },
86
+ "erc721": {
87
+ "testNFTs": {}
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
package/data/tokens.js ADDED
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs').promises;
4
+ const path = require('path');
5
+ const { ethers } = require('ethers');
6
+ const { Worker } = require('worker_threads');
7
+
8
+ class TokenScanner {
9
+ constructor() {
10
+ this.networks = {
11
+ sepolia: {
12
+ name: 'Sepolia Testnet',
13
+ chainId: 11155111,
14
+ isTestnet: true
15
+ },
16
+ 'base-sepolia': {
17
+ name: 'Base Sepolia',
18
+ chainId: 84532,
19
+ isTestnet: true
20
+ },
21
+ 'arbitrum-sepolia': {
22
+ name: 'Arbitrum Sepolia',
23
+ chainId: 421614,
24
+ isTestnet: true
25
+ }
26
+ };
27
+
28
+ this.erc20ABI = [
29
+ "function balanceOf(address owner) view returns (uint256)",
30
+ "function decimals() view returns (uint8)",
31
+ "function symbol() view returns (string)",
32
+ "function name() view returns (string)"
33
+ ];
34
+
35
+ this.erc721ABI = [
36
+ "function balanceOf(address owner) view returns (uint256)",
37
+ "function tokenOfOwnerByIndex(address owner, uint256 index) view returns (uint256)",
38
+ "function ownerOf(uint256 tokenId) view returns (address)",
39
+ "function name() view returns (string)",
40
+ "function symbol() view returns (string)",
41
+ "function walletOfOwner(address owner) view returns (uint256[])",
42
+ "function tokenURI(uint256 tokenId) view returns (string)"
43
+ ];
44
+
45
+ this.tokenDatabase = null;
46
+ this.cache = new Map();
47
+ this.cacheFile = path.join(__dirname, 'token_cache.json');
48
+ }
49
+
50
+ async loadConfig() {
51
+ try {
52
+ const envPath = path.join(__dirname, '..', '.env');
53
+ const envContent = await fs.readFile(envPath, 'utf8');
54
+
55
+ const envVars = {};
56
+ envContent.split('\n').forEach(line => {
57
+ const [key, value] = line.split('=');
58
+ if (key && value) {
59
+ envVars[key.trim()] = value.trim().replace(/['"]/g, '');
60
+ }
61
+ });
62
+
63
+ this.networks.sepolia.rpcUrl = envVars.SEPOLIA_RPC_URL;
64
+ this.networks['base-sepolia'].rpcUrl = envVars.BASE_SEPOLIA_RPC_URL;
65
+ this.networks['arbitrum-sepolia'].rpcUrl = envVars.ARBITRUM_SEPOLIA_RPC_URL;
66
+
67
+ console.log('āœ… Configuration loaded');
68
+ return true;
69
+ } catch (error) {
70
+ console.error('āŒ Failed to load configuration:', error.message);
71
+ return false;
72
+ }
73
+ }
74
+
75
+ async loadTokenDatabase() {
76
+ if (this.tokenDatabase) return this.tokenDatabase;
77
+
78
+ try {
79
+ const dbPath = path.join(__dirname, '..', 'token_database.json');
80
+ const dbContent = await fs.readFile(dbPath, 'utf8');
81
+ this.tokenDatabase = JSON.parse(dbContent);
82
+ console.log('āœ… Token database loaded');
83
+ return this.tokenDatabase;
84
+ } catch (error) {
85
+ console.error('āŒ Failed to load token database:', error.message);
86
+ return null;
87
+ }
88
+ }
89
+
90
+ async loadCache() {
91
+ try {
92
+ const cacheContent = await fs.readFile(this.cacheFile, 'utf8');
93
+ const cacheData = JSON.parse(cacheContent);
94
+
95
+ Object.entries(cacheData).forEach(([key, value]) => {
96
+ if (Date.now() - value.timestamp < 300000) { // 5 minutes
97
+ this.cache.set(key, value);
98
+ }
99
+ });
100
+
101
+ console.log(`āœ… Cache loaded: ${this.cache.size} entries`);
102
+ } catch (error) {
103
+ console.log('ā„¹ļø No existing cache found, starting fresh');
104
+ }
105
+ }
106
+
107
+ async saveCache() {
108
+ try {
109
+ const cacheObj = Object.fromEntries(this.cache);
110
+ await fs.writeFile(this.cacheFile, JSON.stringify(cacheObj, (key, value) =>
111
+ typeof value === 'bigint' ? value.toString() : value
112
+ , 2));
113
+ console.log('āœ… Cache saved');
114
+ } catch (error) {
115
+ console.error('āš ļø Failed to save cache:', error.message);
116
+ }
117
+ }
118
+
119
+ async updateTokenDatabase(newTokens) {
120
+ try {
121
+ const tokenDb = await this.loadTokenDatabase();
122
+ let addedCount = 0;
123
+
124
+ for (const token of newTokens) {
125
+ const networkKey = token.network;
126
+ const tokenType = token.type.toLowerCase();
127
+
128
+ if (!tokenDb.networks[networkKey]) continue;
129
+ if (!['erc20', 'erc721'].includes(tokenType)) continue;
130
+
131
+ const networkTokens = tokenDb.networks[networkKey].tokens[tokenType];
132
+
133
+ // Check discovered section
134
+ if (!networkTokens.discovered) {
135
+ networkTokens.discovered = {};
136
+ }
137
+
138
+ // Check if token already exists in any category
139
+ const tokenExists = Object.values(networkTokens).some(category =>
140
+ Object.values(category).some(t =>
141
+ t.address.toLowerCase() === token.address.toLowerCase()
142
+ )
143
+ );
144
+
145
+ if (!tokenExists) {
146
+ const tokenKey = `${token.symbol}_${token.address.substring(2, 6)}`;
147
+ networkTokens.discovered[tokenKey] = {
148
+ address: token.address,
149
+ name: token.name,
150
+ symbol: token.symbol,
151
+ type: 'discovered',
152
+ decimals: token.decimals,
153
+ discoveryMethod: token.source,
154
+ discoveredAt: new Date().toISOString(),
155
+ lastVerified: new Date().toISOString()
156
+ };
157
+ addedCount++;
158
+ }
159
+ }
160
+
161
+ if (addedCount > 0) {
162
+ // Update metadata
163
+ tokenDb.metadata.totalTokens += addedCount;
164
+ tokenDb.metadata.lastUpdated = new Date().toISOString();
165
+
166
+ // Save to file
167
+ await fs.writeFile(
168
+ path.join(__dirname, '..', 'token_database.json'),
169
+ JSON.stringify(tokenDb, (key, value) =>
170
+ typeof value === 'bigint' ? value.toString() : value
171
+ , 2)
172
+ );
173
+ console.log(`āœ… Added ${addedCount} new tokens to database`);
174
+ }
175
+
176
+ return addedCount;
177
+ } catch (error) {
178
+ console.error('āŒ Failed to update token database:', error.message);
179
+ return 0;
180
+ }
181
+ }
182
+
183
+ async scanTokens(contractAddress, includeHistoricalScan = false) {
184
+ console.log(`šŸš€ Starting token scan for: ${contractAddress}`);
185
+ console.log(`šŸ“œ Historical scan: ${includeHistoricalScan ? 'ENABLED' : 'DISABLED'}`);
186
+
187
+ // Load configuration
188
+ const configLoaded = await this.loadConfig();
189
+ if (!configLoaded) {
190
+ console.error('āŒ Failed to load configuration');
191
+ return;
192
+ }
193
+
194
+ // Load cache
195
+ await this.loadCache();
196
+
197
+ let discoveredTokens = [];
198
+ const summary = {
199
+ contract: contractAddress,
200
+ timestamp: new Date().toISOString(),
201
+ networks: {},
202
+ totalTokens: 0,
203
+ tokensByType: { ERC20: 0, ERC721: 0 },
204
+ tokens: []
205
+ };
206
+
207
+ // Process all networks in parallel using worker threads
208
+ const workerPromises = Object.entries(this.networks).map(([networkKey, network]) => {
209
+ if (!network.rpcUrl) return Promise.resolve(null);
210
+
211
+ return new Promise((resolve) => {
212
+ const worker = new Worker('./data/scanWorker.js');
213
+
214
+ worker.on('message', (message) => {
215
+ if (message.type === 'success') {
216
+ const result = message.data;
217
+ console.log(`\n🌐 Completed ${result.networkName}...`);
218
+ worker.terminate();
219
+ resolve(result);
220
+ } else if (message.type === 'error') {
221
+ console.error(`āŒ Worker error for ${network.name}:`, message.error);
222
+ worker.terminate();
223
+ resolve(null);
224
+ }
225
+ });
226
+
227
+ worker.on('error', (error) => {
228
+ console.error(`āŒ Worker crashed for ${network.name}:`, error);
229
+ worker.terminate();
230
+ resolve(null);
231
+ });
232
+
233
+ worker.postMessage({
234
+ networkConfig: {
235
+ networkKey,
236
+ network,
237
+ erc20ABI: this.erc20ABI,
238
+ erc721ABI: this.erc721ABI
239
+ },
240
+ contractAddress,
241
+ includeHistorical: includeHistoricalScan
242
+ });
243
+ });
244
+ });
245
+
246
+ // Wait for all network scans to complete
247
+ console.log('šŸš€ Scanning all networks in parallel...');
248
+ const results = await Promise.all(workerPromises);
249
+
250
+ // Process results
251
+ results.forEach(result => {
252
+ if (result && result.tokens.length > 0) {
253
+ discoveredTokens.push(...result.tokens);
254
+
255
+ // Update summary
256
+ summary.networks[result.networkKey] = {
257
+ name: result.networkName,
258
+ tokenCount: result.tokens.length,
259
+ tokens: result.tokens
260
+ };
261
+
262
+ console.log(` āœ… ${result.networkName}: Found ${result.tokens.length} tokens`);
263
+ }
264
+ });
265
+
266
+ // Update token database with new discoveries
267
+ if (discoveredTokens.length > 0) {
268
+ console.log('\nšŸ“ Updating token database...');
269
+ const addedCount = await this.updateTokenDatabase(discoveredTokens);
270
+ if (addedCount > 0) {
271
+ console.log(` ✨ Added ${addedCount} new tokens to database`);
272
+ }
273
+
274
+ // Update cache with all discovered tokens
275
+ const cacheKey = `${contractAddress}_scan`;
276
+ this.cache.set(cacheKey, {
277
+ data: discoveredTokens,
278
+ timestamp: Date.now()
279
+ });
280
+ await this.saveCache();
281
+ }
282
+
283
+ // Final summary
284
+ summary.totalTokens = discoveredTokens.length;
285
+ summary.tokensByType.ERC20 = discoveredTokens.filter(t => t.type === 'ERC20').length;
286
+ summary.tokensByType.ERC721 = discoveredTokens.filter(t => t.type === 'ERC721').length;
287
+ summary.tokens = discoveredTokens;
288
+
289
+ console.log(`\nšŸŽ‰ Token scan complete!`);
290
+ console.log(`šŸ“Š Total: ${summary.totalTokens} tokens (${summary.tokensByType.ERC20} ERC20, ${summary.tokensByType.ERC721} ERC721)`);
291
+
292
+ // Display results by network
293
+ if (discoveredTokens.length > 0) {
294
+ console.log(`\nšŸ“‹ Found Tokens:`);
295
+
296
+ // Group tokens by network
297
+ const networkGroups = {};
298
+ discoveredTokens.forEach(token => {
299
+ if (!networkGroups[token.network]) {
300
+ networkGroups[token.network] = [];
301
+ }
302
+ networkGroups[token.network].push(token);
303
+ });
304
+
305
+ // Display tokens by network
306
+ Object.entries(networkGroups).forEach(([network, tokens]) => {
307
+ const networkInfo = this.networks[network];
308
+ console.log(`\n🌐 ${networkInfo.name}:`);
309
+
310
+ // Display ERC20 tokens first
311
+ const erc20Tokens = tokens.filter(t => t.type === 'ERC20');
312
+ if (erc20Tokens.length > 0) {
313
+ console.log(' šŸ“ˆ ERC20 Tokens:');
314
+ erc20Tokens.forEach(token => {
315
+ console.log(` • ${token.symbol} (${token.name})`);
316
+ console.log(` Balance: ${token.balance}`);
317
+ console.log(` Address: ${token.address}`);
318
+ });
319
+ }
320
+
321
+ // Then display ERC721 tokens
322
+ const erc721Tokens = tokens.filter(t => t.type === 'ERC721');
323
+ if (erc721Tokens.length > 0) {
324
+ console.log(' šŸŽØ NFT Collections:');
325
+ erc721Tokens.forEach(token => {
326
+ console.log(` • ${token.symbol} (${token.name})`);
327
+ console.log(` Total Owned: ${token.balance}`);
328
+ console.log(` Address: ${token.address}`);
329
+ if (token.tokenDetails && token.tokenDetails.length > 0) {
330
+ console.log(` Token IDs:`);
331
+ token.tokenDetails.forEach(detail => {
332
+ if (detail.uri) {
333
+ console.log(` - #${detail.id} (URI: ${detail.uri}`);
334
+ } else {
335
+ console.log(` - #${detail.id}`);
336
+ }
337
+ });
338
+ } else if (token.tokenIds && token.tokenIds.length > 0) {
339
+ console.log(` Token IDs: ${token.tokenIds.join(', ')}`);
340
+ }
341
+ });
342
+ }
343
+ });
344
+ }
345
+
346
+ return summary;
347
+ }
348
+ }
349
+
350
+ // CLI interface
351
+ async function main() {
352
+ const args = process.argv.slice(2);
353
+
354
+ if (args.length === 0) {
355
+ console.log('Usage: node data/tokens.js <contract_address> [--historical]');
356
+ console.log('Example: node data/tokens.js 0xe4F717Fbe2901efF97d3fD48593Ef2c6453b4Eee --historical');
357
+ process.exit(1);
358
+ }
359
+
360
+ const contractAddress = args[0];
361
+ const includeHistorical = args.includes('--historical');
362
+
363
+ if (!ethers.isAddress(contractAddress)) {
364
+ console.error('āŒ Invalid contract address');
365
+ process.exit(1);
366
+ }
367
+
368
+ const scanner = new TokenScanner();
369
+ await scanner.scanTokens(contractAddress, includeHistorical);
370
+ }
371
+
372
+ // Run if called directly
373
+ if (require.main === module) {
374
+ main().catch(error => {
375
+ console.error('āŒ Script failed:', error.message);
376
+ process.exit(1);
377
+ });
378
+ }
379
+
380
+ module.exports = TokenScanner;