@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.
- package/CHANGELOG.md +6 -0
- package/lib/generatedAssetData.json +61276 -147584
- package/package.json +1 -1
- package/scripts/.coingecko-failed.json +46 -0
- package/scripts/.coingecko-progress.json +9 -0
- package/scripts/README-ICON-DOWNLOAD.md +220 -0
- package/scripts/download-with-coingecko-api.js +414 -0
- package/scripts/test-coingecko-download.js +317 -0
- package/scripts/test-sample.json +37 -0
|
@@ -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
|
+
]
|