@pioneer-platform/pioneer-discovery 4.21.15 → 4.21.16

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,317 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * TEST VERSION - Enhanced Icon Download with CoinGecko API
4
+ * Uses test-sample.json (5 assets) for quick testing
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const https = require('https');
10
+ const http = require('http');
11
+
12
+ const LOCAL_COINS_DIR = path.join(__dirname, '..', '..', '..', '..', '..', 'services', 'pioneer-server', 'public', 'coins');
13
+ const MISSING_ASSETS_FILE = path.join(__dirname, 'test-sample.json');
14
+
15
+ // API rate limiting
16
+ const API_DELAY_MS = 1500; // 1.5 seconds between API calls
17
+ const MAX_RETRIES = 3;
18
+
19
+ // Ensure local coins directory exists
20
+ if (!fs.existsSync(LOCAL_COINS_DIR)) {
21
+ fs.mkdirSync(LOCAL_COINS_DIR, { recursive: true });
22
+ }
23
+
24
+ /**
25
+ * Map CAIP chainId to CoinGecko platform identifier
26
+ */
27
+ const CHAIN_ID_TO_COINGECKO_PLATFORM = {
28
+ 'eip155:1': 'ethereum',
29
+ 'eip155:10': 'optimism',
30
+ 'eip155:56': 'binance-smart-chain',
31
+ 'eip155:100': 'xdai',
32
+ 'eip155:137': 'polygon-pos',
33
+ 'eip155:250': 'fantom',
34
+ 'eip155:8453': 'base',
35
+ 'eip155:42161': 'arbitrum-one',
36
+ 'eip155:42220': 'celo',
37
+ 'eip155:43114': 'avalanche',
38
+ 'eip155:534352': 'scroll',
39
+ 'eip155:59144': 'linea',
40
+ 'eip155:324': 'zksync',
41
+ 'eip155:1101': 'polygon-zkevm',
42
+ 'eip155:5000': 'mantle',
43
+ 'eip155:81457': 'blast',
44
+ };
45
+
46
+ /**
47
+ * Convert assetId to base64-encoded format
48
+ */
49
+ function encodeAssetId(assetId) {
50
+ return Buffer.from(assetId).toString('base64');
51
+ }
52
+
53
+ /**
54
+ * Extract contract address from assetId
55
+ */
56
+ function extractContractAddress(assetId) {
57
+ const match = assetId.match(/erc20:(0x[a-fA-F0-9]{40})/);
58
+ return match ? match[1].toLowerCase() : null;
59
+ }
60
+
61
+ /**
62
+ * Sleep for specified milliseconds
63
+ */
64
+ function sleep(ms) {
65
+ return new Promise(resolve => setTimeout(resolve, ms));
66
+ }
67
+
68
+ /**
69
+ * Make HTTP/HTTPS request with timeout
70
+ */
71
+ async function makeRequest(url, timeout = 10000) {
72
+ return new Promise((resolve, reject) => {
73
+ try {
74
+ const urlObj = new URL(url);
75
+ const client = urlObj.protocol === 'https:' ? https : http;
76
+
77
+ const req = client.get(url, { timeout }, (res) => {
78
+ let data = '';
79
+
80
+ res.on('data', chunk => {
81
+ data += chunk;
82
+ });
83
+
84
+ res.on('end', () => {
85
+ if (res.statusCode >= 200 && res.statusCode < 300) {
86
+ try {
87
+ const parsed = JSON.parse(data);
88
+ resolve({ success: true, data: parsed, statusCode: res.statusCode });
89
+ } catch (e) {
90
+ resolve({ success: false, error: 'Invalid JSON', statusCode: res.statusCode });
91
+ }
92
+ } else if (res.statusCode === 429) {
93
+ resolve({ success: false, error: 'Rate limited', statusCode: 429 });
94
+ } else if (res.statusCode === 404) {
95
+ resolve({ success: false, error: 'Not found', statusCode: 404 });
96
+ } else {
97
+ resolve({ success: false, error: `HTTP ${res.statusCode}`, statusCode: res.statusCode });
98
+ }
99
+ });
100
+ });
101
+
102
+ req.on('error', (err) => {
103
+ resolve({ success: false, error: err.message });
104
+ });
105
+
106
+ req.on('timeout', () => {
107
+ req.destroy();
108
+ resolve({ success: false, error: 'Timeout' });
109
+ });
110
+ } catch (e) {
111
+ resolve({ success: false, error: e.message });
112
+ }
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Download file from URL
118
+ */
119
+ async function downloadFile(url, outputPath, timeout = 10000) {
120
+ return new Promise((resolve) => {
121
+ try {
122
+ const urlObj = new URL(url);
123
+ const client = urlObj.protocol === 'https:' ? https : http;
124
+
125
+ const req = client.get(url, { timeout }, (res) => {
126
+ if (res.statusCode >= 200 && res.statusCode < 300) {
127
+ const fileStream = fs.createWriteStream(outputPath);
128
+ res.pipe(fileStream);
129
+ fileStream.on('finish', () => {
130
+ fileStream.close();
131
+ resolve(true);
132
+ });
133
+ fileStream.on('error', () => resolve(false));
134
+ } else if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
135
+ // Follow redirect
136
+ downloadFile(res.headers.location, outputPath, timeout).then(resolve);
137
+ } else {
138
+ resolve(false);
139
+ }
140
+ });
141
+
142
+ req.on('error', () => resolve(false));
143
+ req.on('timeout', () => {
144
+ req.destroy();
145
+ resolve(false);
146
+ });
147
+ } catch (e) {
148
+ resolve(false);
149
+ }
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Query CoinGecko API for token info by contract address
155
+ */
156
+ async function getCoinGeckoTokenInfo(chainId, contractAddress, retries = 0) {
157
+ const platform = CHAIN_ID_TO_COINGECKO_PLATFORM[chainId];
158
+ if (!platform) {
159
+ return { success: false, error: 'Unsupported chain' };
160
+ }
161
+
162
+ const url = `https://api.coingecko.com/api/v3/coins/${platform}/contract/${contractAddress}`;
163
+ console.log(` 🔗 API URL: ${url}`);
164
+
165
+ const result = await makeRequest(url);
166
+
167
+ // Handle rate limiting with exponential backoff
168
+ if (result.statusCode === 429 && retries < MAX_RETRIES) {
169
+ const backoffMs = API_DELAY_MS * Math.pow(2, retries);
170
+ console.log(` ⏳ Rate limited, waiting ${backoffMs}ms...`);
171
+ await sleep(backoffMs);
172
+ return getCoinGeckoTokenInfo(chainId, contractAddress, retries + 1);
173
+ }
174
+
175
+ return result;
176
+ }
177
+
178
+ /**
179
+ * Get TrustWallet fallback URLs
180
+ */
181
+ function getTrustWalletFallbacks(contractAddress) {
182
+ // Simple checksum approach
183
+ const checksummed = contractAddress.substring(0, 2) + contractAddress.substring(2).split('')
184
+ .map((char, i) => i % 2 === 0 && parseInt(char, 16) >= 8 ? char.toUpperCase() : char)
185
+ .join('');
186
+
187
+ return [`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${checksummed}/logo.png`];
188
+ }
189
+
190
+ /**
191
+ * Process a single asset with CoinGecko API
192
+ */
193
+ async function processAsset(asset, index, total, stats) {
194
+ const { assetId, symbol, name, chainId } = asset;
195
+ const encoded = encodeAssetId(assetId);
196
+ const localPath = path.join(LOCAL_COINS_DIR, `${encoded}.png`);
197
+
198
+ console.log(`\n[${ index + 1}/${total}] ${symbol} - ${name}`);
199
+ console.log(` Asset: ${assetId}`);
200
+
201
+ // Check local file first
202
+ if (fs.existsSync(localPath)) {
203
+ const fileSize = fs.statSync(localPath).size;
204
+ if (fileSize > 0) {
205
+ console.log(` ✅ Already exists locally (${fileSize} bytes)`);
206
+ stats.alreadyLocal++;
207
+ return true;
208
+ }
209
+ }
210
+
211
+ // Extract contract address
212
+ const contractAddress = extractContractAddress(assetId);
213
+ if (!contractAddress) {
214
+ console.log(` ⚠️ Not an ERC20 token`);
215
+ stats.notErc20++;
216
+ return false;
217
+ }
218
+
219
+ console.log(` 📍 Contract: ${contractAddress}`);
220
+
221
+ // Query CoinGecko API
222
+ console.log(` 🔍 Querying CoinGecko API...`);
223
+ const result = await getCoinGeckoTokenInfo(chainId, contractAddress);
224
+
225
+ // Respect rate limits
226
+ await sleep(API_DELAY_MS);
227
+
228
+ if (result.success && result.data?.image) {
229
+ const imageUrl = result.data.image.large || result.data.image.small || result.data.image.thumb;
230
+
231
+ if (imageUrl) {
232
+ console.log(` 📥 Downloading: ${imageUrl}`);
233
+ const downloaded = await downloadFile(imageUrl, localPath);
234
+
235
+ if (downloaded) {
236
+ const fileSize = fs.statSync(localPath).size;
237
+ console.log(` ✅ Successfully downloaded (${fileSize} bytes)`);
238
+ stats.downloadedFromCoinGecko++;
239
+ return true;
240
+ } else {
241
+ console.log(` ❌ Download failed`);
242
+ }
243
+ }
244
+ } else {
245
+ console.log(` ℹ️ CoinGecko: ${result.error || 'Not found'} (HTTP ${result.statusCode || 'N/A'})`);
246
+ }
247
+
248
+ // Try TrustWallet fallback
249
+ const trustWalletUrls = getTrustWalletFallbacks(contractAddress);
250
+ for (const url of trustWalletUrls) {
251
+ console.log(` 🔄 Trying TrustWallet: ${url.substring(0, 80)}...`);
252
+ const downloaded = await downloadFile(url, localPath);
253
+ if (downloaded) {
254
+ const fileSize = fs.statSync(localPath).size;
255
+ console.log(` ✅ Downloaded from TrustWallet (${fileSize} bytes)`);
256
+ stats.downloadedFromTrustWallet++;
257
+ return true;
258
+ }
259
+ }
260
+
261
+ console.log(` ❌ No icon found`);
262
+ stats.failed++;
263
+ return false;
264
+ }
265
+
266
+ /**
267
+ * Main execution
268
+ */
269
+ async function main() {
270
+ console.log('🧪 TEST MODE - Enhanced icon download with CoinGecko API\n');
271
+ console.log('Testing with 5 sample assets from test-sample.json\n');
272
+
273
+ // Load test sample
274
+ if (!fs.existsSync(MISSING_ASSETS_FILE)) {
275
+ console.log('❌ test-sample.json not found');
276
+ process.exit(1);
277
+ }
278
+
279
+ const assets = JSON.parse(fs.readFileSync(MISSING_ASSETS_FILE, 'utf8'));
280
+ console.log(`📂 Loaded ${assets.length} test assets\n`);
281
+
282
+ const stats = {
283
+ alreadyLocal: 0,
284
+ downloadedFromCoinGecko: 0,
285
+ downloadedFromTrustWallet: 0,
286
+ notErc20: 0,
287
+ failed: 0,
288
+ };
289
+
290
+ // Process each asset
291
+ for (let i = 0; i < assets.length; i++) {
292
+ await processAsset(assets[i], i, assets.length, stats);
293
+ }
294
+
295
+ // Print summary
296
+ console.log('\n' + '='.repeat(80));
297
+ console.log('📊 TEST SUMMARY');
298
+ console.log('='.repeat(80));
299
+ console.log(`Total assets: ${assets.length}`);
300
+ console.log(`Already local: ${stats.alreadyLocal}`);
301
+ console.log(`Downloaded from CoinGecko: ${stats.downloadedFromCoinGecko}`);
302
+ console.log(`Downloaded from TrustWallet: ${stats.downloadedFromTrustWallet}`);
303
+ console.log(`Not ERC20 (skipped): ${stats.notErc20}`);
304
+ console.log(`Failed: ${stats.failed}`);
305
+ console.log('='.repeat(80));
306
+
307
+ const successRate = ((stats.downloadedFromCoinGecko + stats.downloadedFromTrustWallet) / assets.length * 100).toFixed(1);
308
+ console.log(`\n✅ Success rate: ${successRate}%\n`);
309
+
310
+ const localFiles = fs.readdirSync(LOCAL_COINS_DIR).filter(f => f.endsWith('.png')).length;
311
+ console.log(`📁 Local coins directory: ${localFiles} PNG files\n`);
312
+ }
313
+
314
+ main().catch(err => {
315
+ console.error('❌ Error:', err);
316
+ process.exit(1);
317
+ });
@@ -0,0 +1,37 @@
1
+ [
2
+ {
3
+ "assetId": "eip155:1/erc20:0x0173661769325565d4f011b2e5cda688689cc87c",
4
+ "symbol": "QLT",
5
+ "name": "Quantland",
6
+ "chainId": "eip155:1",
7
+ "oldIcon": "https://api.keepkey.info/coins/ZWlwMTU1OjEvZXJjMjA6MHgwMTczNjYxNzY5MzI1NTY1ZDRmMDExYjJlNWNkYTY4ODY4OWNjODdj.png"
8
+ },
9
+ {
10
+ "assetId": "eip155:1/erc20:0x01ba67aac7f75f647d94220cc98fb30fcc5105bf",
11
+ "symbol": "LYRA",
12
+ "name": "Lyra Finance on Ethereum",
13
+ "chainId": "eip155:1",
14
+ "oldIcon": "https://api.keepkey.info/coins/ZWlwMTU1OjEvZXJjMjA6MHgwMWJhNjdhYWM3Zjc1ZjY0N2Q5NDIyMGNjOThmYjMwZmNjNTEwNWJm.png"
15
+ },
16
+ {
17
+ "assetId": "eip155:1/erc20:0x0198f46f520f33cd4329bd4be380a25a90536cd5",
18
+ "symbol": "PLA",
19
+ "name": "PlayChip",
20
+ "chainId": "eip155:1",
21
+ "oldIcon": "https://api.keepkey.info/coins/ZWlwMTU1OjEvZXJjMjA6MHgwMTk4ZjQ2ZjUyMGYzM2NkNDMyOWJkNGJlMzgwYTI1YTkwNTM2Y2Q1.png"
22
+ },
23
+ {
24
+ "assetId": "eip155:1/erc20:0x01792e1548dc317bde6123fe92da1fe6d7311c3c",
25
+ "symbol": "SPIRAL",
26
+ "name": "Spiral",
27
+ "chainId": "eip155:1",
28
+ "oldIcon": "https://api.keepkey.info/coins/ZWlwMTU1OjEvZXJjMjA6MHgwMTc5MmUxNTQ4ZGMzMTdiZGU2MTIzZmU5MmRhMWZlNmQ3MzExYzNj.png"
29
+ },
30
+ {
31
+ "assetId": "eip155:1/erc20:0x0176b898e92e814c06cc379e508ceb571f70bd40",
32
+ "symbol": "TIP",
33
+ "name": "Tipcoin",
34
+ "chainId": "eip155:1",
35
+ "oldIcon": "https://api.keepkey.info/coins/ZWlwMTU1OjEvZXJjMjA6MHgwMTc2Yjg5OGU5MmU4MTRjMDZjYzM3OWU1MDhjZWI1NzFmNzBiZDQw.png"
36
+ }
37
+ ]