@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.
- package/.claude/settings.local.json +12 -0
- package/.turbo/turbo-build.log +2 -1
- package/CHANGELOG.md +12 -0
- package/lib/generatedAssetData.json +14221 -14217
- package/package.json +1 -1
- package/scripts/missing-assets.json +99101 -0
- package/scripts/update-icon-urls.js +86 -0
- package/scripts/validate-and-download-icons-fast.js +304 -0
- package/scripts/validate-and-download-icons.js +326 -0
|
@@ -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
|
+
});
|