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.
- package/README.md +514 -0
- package/cli/commands/account/index.js +855 -0
- package/cli/commands/config/index.js +369 -0
- package/cli/commands/contact/index.js +139 -0
- package/cli/commands/contract/index.js +197 -0
- package/cli/commands/data/index.js +536 -0
- package/cli/commands/tx/index.js +841 -0
- package/cli/commands/wallet/index.js +181 -0
- package/cli/index.js +624 -0
- package/cli/utils/auth.js +146 -0
- package/cli/utils/constants.js +68 -0
- package/cli/utils/contacts.js +131 -0
- package/cli/utils/contracts.js +269 -0
- package/cli/utils/crosschain.js +278 -0
- package/cli/utils/networks.js +335 -0
- package/cli/utils/notifications.js +135 -0
- package/cli/utils/output.js +123 -0
- package/cli/utils/pin.js +89 -0
- package/data/balance.js +680 -0
- package/data/events.js +334 -0
- package/data/pending.js +261 -0
- package/data/scanWorker.js +169 -0
- package/data/token_cache.json +54 -0
- package/data/token_database.json +92 -0
- package/data/tokens.js +380 -0
- package/package.json +57 -0
|
@@ -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;
|