@pioneer-platform/pioneer-discovery 0.8.3 → 4.21.15

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,326 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Validate and Download Icon URLs
4
+ *
5
+ * Priority order:
6
+ * 1. Check if api.keepkey.com/coins/{base64_caip}.png exists
7
+ * 2. Check if local file exists in pioneer-server/public/coins/
8
+ * 3. Try old URL (CoinGecko, TrustWallet, etc.)
9
+ * 4. Try known fallbacks (CoinGecko API search, symbol-based)
10
+ * 5. Log missing assets for manual resolution
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const https = require('https');
16
+ const http = require('http');
17
+
18
+ const DATA_FILE = path.join(__dirname, '..', 'src', 'generatedAssetData.json');
19
+ const LOCAL_COINS_DIR = path.join(__dirname, '..', '..', '..', '..', '..', 'services', 'pioneer-server', 'public', 'coins');
20
+ const MISSING_ASSETS_FILE = path.join(__dirname, 'missing-assets.json');
21
+
22
+ // Ensure local coins directory exists
23
+ if (!fs.existsSync(LOCAL_COINS_DIR)) {
24
+ console.log(`šŸ“ Creating directory: ${LOCAL_COINS_DIR}`);
25
+ fs.mkdirSync(LOCAL_COINS_DIR, { recursive: true });
26
+ }
27
+
28
+ /**
29
+ * Convert assetId to base64-encoded format
30
+ */
31
+ function encodeAssetId(assetId) {
32
+ return Buffer.from(assetId).toString('base64');
33
+ }
34
+
35
+ /**
36
+ * Check if URL exists (HEAD request)
37
+ */
38
+ async function urlExists(url, timeout = 5000) {
39
+ return new Promise((resolve) => {
40
+ const urlObj = new URL(url);
41
+ const client = urlObj.protocol === 'https:' ? https : http;
42
+
43
+ const req = client.request(url, { method: 'HEAD', timeout }, (res) => {
44
+ resolve(res.statusCode >= 200 && res.statusCode < 400);
45
+ });
46
+
47
+ req.on('error', () => resolve(false));
48
+ req.on('timeout', () => {
49
+ req.destroy();
50
+ resolve(false);
51
+ });
52
+
53
+ req.end();
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Download file from URL
59
+ */
60
+ async function downloadFile(url, outputPath) {
61
+ return new Promise((resolve, reject) => {
62
+ const urlObj = new URL(url);
63
+ const client = urlObj.protocol === 'https:' ? https : http;
64
+
65
+ const req = client.get(url, { timeout: 10000 }, (res) => {
66
+ if (res.statusCode >= 200 && res.statusCode < 300) {
67
+ const fileStream = fs.createWriteStream(outputPath);
68
+ res.pipe(fileStream);
69
+ fileStream.on('finish', () => {
70
+ fileStream.close();
71
+ resolve(true);
72
+ });
73
+ } else if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
74
+ // Follow redirect
75
+ downloadFile(res.headers.location, outputPath).then(resolve).catch(reject);
76
+ } else {
77
+ resolve(false);
78
+ }
79
+ });
80
+
81
+ req.on('error', () => resolve(false));
82
+ req.on('timeout', () => {
83
+ req.destroy();
84
+ resolve(false);
85
+ });
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Extract symbol/contract for fallback searches
91
+ */
92
+ function extractSymbol(assetData) {
93
+ return assetData.symbol?.toLowerCase() || '';
94
+ }
95
+
96
+ /**
97
+ * Try various CoinGecko fallback URLs
98
+ */
99
+ function getCoinGeckoFallbacks(assetData) {
100
+ const symbol = extractSymbol(assetData);
101
+ const fallbacks = [];
102
+
103
+ // Common CoinGecko patterns
104
+ if (symbol) {
105
+ fallbacks.push(`https://assets.coingecko.com/coins/images/1/small/${symbol}.png`);
106
+ fallbacks.push(`https://assets.coingecko.com/coins/images/1/large/${symbol}.png`);
107
+ fallbacks.push(`https://assets.coingecko.com/coins/images/1/thumb/${symbol}.png`);
108
+ }
109
+
110
+ // Common coins with known CoinGecko IDs
111
+ const knownIds = {
112
+ 'btc': 'https://assets.coingecko.com/coins/images/1/small/bitcoin.png',
113
+ 'eth': 'https://assets.coingecko.com/coins/images/279/large/ethereum.png',
114
+ 'usdc': 'https://assets.coingecko.com/coins/images/6319/large/usdc.png',
115
+ 'usdt': 'https://assets.coingecko.com/coins/images/325/large/tether.png',
116
+ 'dai': 'https://assets.coingecko.com/coins/images/9956/large/dai.png',
117
+ 'wbtc': 'https://assets.coingecko.com/coins/images/7598/large/wrapped_bitcoin.png',
118
+ 'link': 'https://assets.coingecko.com/coins/images/877/large/chainlink.png',
119
+ 'uni': 'https://assets.coingecko.com/coins/images/12504/large/uniswap.png',
120
+ 'atom': 'https://assets.coingecko.com/coins/images/1481/large/cosmos_hub.png',
121
+ 'osmo': 'https://assets.coingecko.com/coins/images/16724/large/osmosis.png',
122
+ 'rune': 'https://assets.coingecko.com/coins/images/6595/large/thorchain.png',
123
+ 'cacao': 'https://assets.coingecko.com/coins/images/27929/large/cacao.png',
124
+ };
125
+
126
+ if (knownIds[symbol]) {
127
+ fallbacks.push(knownIds[symbol]);
128
+ }
129
+
130
+ return fallbacks;
131
+ }
132
+
133
+ /**
134
+ * Try TrustWallet fallbacks
135
+ */
136
+ function getTrustWalletFallbacks(assetData) {
137
+ const fallbacks = [];
138
+
139
+ // Extract contract address for ERC20 tokens
140
+ if (assetData.assetId?.includes('erc20:')) {
141
+ const match = assetData.assetId.match(/erc20:(0x[a-fA-F0-9]+)/);
142
+ if (match) {
143
+ const contract = match[1];
144
+ fallbacks.push(`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${contract}/logo.png`);
145
+ fallbacks.push(`https://rawcdn.githack.com/trustwallet/assets/master/blockchains/ethereum/assets/${contract}/logo.png`);
146
+ }
147
+ }
148
+
149
+ return fallbacks;
150
+ }
151
+
152
+ /**
153
+ * Try all fallback sources
154
+ */
155
+ async function tryFallbacks(assetData) {
156
+ const fallbacks = [
157
+ ...getCoinGeckoFallbacks(assetData),
158
+ ...getTrustWalletFallbacks(assetData),
159
+ ];
160
+
161
+ for (const url of fallbacks) {
162
+ const exists = await urlExists(url);
163
+ if (exists) {
164
+ return url;
165
+ }
166
+ }
167
+
168
+ return null;
169
+ }
170
+
171
+ /**
172
+ * Process a single asset
173
+ */
174
+ async function processAsset(assetId, assetData, stats) {
175
+ const encoded = encodeAssetId(assetId);
176
+ const targetUrl = `https://api.keepkey.com/coins/${encoded}.png`;
177
+ const localPath = path.join(LOCAL_COINS_DIR, `${encoded}.png`);
178
+
179
+ stats.total++;
180
+
181
+ // Step 1: Check if api.keepkey.com already has it
182
+ console.log(`\nšŸ” [${stats.total}] Checking ${assetData.symbol} (${assetId.substring(0, 40)}...)`);
183
+
184
+ const remoteExists = await urlExists(targetUrl);
185
+ if (remoteExists) {
186
+ console.log(` āœ… Already on api.keepkey.com`);
187
+ stats.alreadyOnRemote++;
188
+ assetData.icon = targetUrl;
189
+ return true;
190
+ }
191
+
192
+ // Step 2: Check if local file exists
193
+ if (fs.existsSync(localPath)) {
194
+ console.log(` āœ… Already in local storage (not deployed yet)`);
195
+ stats.alreadyLocal++;
196
+ assetData.icon = targetUrl;
197
+ return true;
198
+ }
199
+
200
+ // Step 3: Try old URL if it exists
201
+ if (assetData.icon && !assetData.icon.includes('api.keepkey.com')) {
202
+ const oldUrl = assetData.icon;
203
+ console.log(` šŸ”„ Trying old URL: ${oldUrl.substring(0, 60)}...`);
204
+
205
+ const oldExists = await urlExists(oldUrl);
206
+ if (oldExists) {
207
+ console.log(` šŸ“„ Downloading from old URL...`);
208
+ const downloaded = await downloadFile(oldUrl, localPath);
209
+ if (downloaded) {
210
+ console.log(` āœ… Downloaded and saved locally`);
211
+ stats.downloaded++;
212
+ assetData.icon = targetUrl;
213
+ return true;
214
+ } else {
215
+ console.log(` āŒ Failed to download from old URL`);
216
+ }
217
+ } else {
218
+ console.log(` āŒ Old URL not accessible`);
219
+ }
220
+ }
221
+
222
+ // Step 4: Try known fallbacks
223
+ console.log(` šŸ”„ Trying fallback sources...`);
224
+ const fallbackUrl = await tryFallbacks(assetData);
225
+ if (fallbackUrl) {
226
+ console.log(` šŸ“„ Found fallback: ${fallbackUrl.substring(0, 60)}...`);
227
+ const downloaded = await downloadFile(fallbackUrl, localPath);
228
+ if (downloaded) {
229
+ console.log(` āœ… Downloaded from fallback and saved locally`);
230
+ stats.downloadedFallback++;
231
+ assetData.icon = targetUrl;
232
+ return true;
233
+ } else {
234
+ console.log(` āŒ Failed to download from fallback`);
235
+ }
236
+ }
237
+
238
+ // Step 5: Mark as missing
239
+ console.log(` āŒ No icon found - marked as missing`);
240
+ stats.missing.push({
241
+ assetId,
242
+ symbol: assetData.symbol,
243
+ name: assetData.name,
244
+ chainId: assetData.chainId,
245
+ oldIcon: assetData.icon || null,
246
+ });
247
+ stats.missingCount++;
248
+
249
+ // Keep the target URL even if file doesn't exist yet
250
+ assetData.icon = targetUrl;
251
+ return false;
252
+ }
253
+
254
+ /**
255
+ * Main execution
256
+ */
257
+ async function main() {
258
+ console.log('šŸš€ Starting icon validation and download process...\n');
259
+ console.log(`šŸ“‚ Local coins directory: ${LOCAL_COINS_DIR}\n`);
260
+
261
+ const data = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
262
+ const entries = Object.entries(data);
263
+
264
+ const stats = {
265
+ total: 0,
266
+ alreadyOnRemote: 0,
267
+ alreadyLocal: 0,
268
+ downloaded: 0,
269
+ downloadedFallback: 0,
270
+ missingCount: 0,
271
+ missing: [],
272
+ };
273
+
274
+ // Process assets with concurrency limit
275
+ const concurrency = 5;
276
+ for (let i = 0; i < entries.length; i += concurrency) {
277
+ const batch = entries.slice(i, i + concurrency);
278
+ await Promise.all(
279
+ batch.map(([assetId, assetData]) => processAsset(assetId, assetData, stats))
280
+ );
281
+ }
282
+
283
+ // Save updated data
284
+ console.log('\n\nšŸ’¾ Saving updated asset data...');
285
+ fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf8');
286
+
287
+ // Save missing assets report
288
+ if (stats.missing.length > 0) {
289
+ console.log(`šŸ“ Saving missing assets report to ${MISSING_ASSETS_FILE}...`);
290
+ fs.writeFileSync(MISSING_ASSETS_FILE, JSON.stringify(stats.missing, null, 2), 'utf8');
291
+ }
292
+
293
+ // Print summary
294
+ console.log('\n' + '='.repeat(80));
295
+ console.log('šŸ“Š SUMMARY REPORT');
296
+ console.log('='.repeat(80));
297
+ console.log(`Total assets processed: ${stats.total}`);
298
+ console.log(`Already on api.keepkey.com: ${stats.alreadyOnRemote} (${(stats.alreadyOnRemote/stats.total*100).toFixed(1)}%)`);
299
+ console.log(`Already in local storage: ${stats.alreadyLocal} (${(stats.alreadyLocal/stats.total*100).toFixed(1)}%)`);
300
+ console.log(`Downloaded from old URL: ${stats.downloaded}`);
301
+ console.log(`Downloaded from fallback: ${stats.downloadedFallback}`);
302
+ console.log(`Missing (needs manual work): ${stats.missingCount} (${(stats.missingCount/stats.total*100).toFixed(1)}%)`);
303
+ console.log('='.repeat(80));
304
+
305
+ if (stats.missing.length > 0) {
306
+ console.log('\nāš ļø Missing assets by chain:');
307
+ const byChain = {};
308
+ stats.missing.forEach(asset => {
309
+ const chain = asset.chainId || 'unknown';
310
+ byChain[chain] = (byChain[chain] || 0) + 1;
311
+ });
312
+ Object.entries(byChain)
313
+ .sort((a, b) => b[1] - a[1])
314
+ .forEach(([chain, count]) => {
315
+ console.log(` ${chain}: ${count} missing`);
316
+ });
317
+ console.log(`\n Full report saved to: ${MISSING_ASSETS_FILE}`);
318
+ }
319
+
320
+ console.log('\nāœ… Process complete!\n');
321
+ }
322
+
323
+ main().catch(err => {
324
+ console.error('āŒ Fatal error:', err);
325
+ process.exit(1);
326
+ });