|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class APIResourceLoader {
|
|
|
constructor() {
|
|
|
this.resources = {
|
|
|
unified: null,
|
|
|
ultimate: null,
|
|
|
config: null
|
|
|
};
|
|
|
this.cache = new Map();
|
|
|
this.initialized = false;
|
|
|
this.failedResources = new Set();
|
|
|
this.initPromise = null;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async init() {
|
|
|
|
|
|
if (this.initPromise) {
|
|
|
return this.initPromise;
|
|
|
}
|
|
|
|
|
|
|
|
|
if (this.initialized) {
|
|
|
return this.resources;
|
|
|
}
|
|
|
|
|
|
|
|
|
this.initPromise = (async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
|
const [unified, ultimate, config] = await Promise.allSettled([
|
|
|
this.loadResource('/api-resources/crypto_resources_unified_2025-11-11.json').catch(() => null),
|
|
|
this.loadResource('/api-resources/ultimate_crypto_pipeline_2025_NZasinich.json').catch(() => null),
|
|
|
this.loadResource('/api-resources/api-config-complete__1_.txt')
|
|
|
.then(text => {
|
|
|
|
|
|
if (typeof text === 'string' && text.trim()) {
|
|
|
return this.parseConfigText(text);
|
|
|
}
|
|
|
return null;
|
|
|
})
|
|
|
.catch(() => null)
|
|
|
]);
|
|
|
|
|
|
|
|
|
if (unified.status === 'fulfilled' && unified.value) {
|
|
|
this.resources.unified = unified.value;
|
|
|
const count = this.resources.unified?.registry?.metadata?.total_entries || 0;
|
|
|
if (count > 0) {
|
|
|
console.log('[API Resource Loader] Unified resources loaded:', count, 'entries');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if (ultimate.status === 'fulfilled' && ultimate.value) {
|
|
|
this.resources.ultimate = ultimate.value;
|
|
|
const count = this.resources.ultimate?.total_sources || 0;
|
|
|
if (count > 0) {
|
|
|
console.log('[API Resource Loader] Ultimate resources loaded:', count, 'sources');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if (config.status === 'fulfilled' && config.value) {
|
|
|
this.resources.config = config.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
this.initialized = true;
|
|
|
|
|
|
|
|
|
const stats = this.getStats();
|
|
|
if (stats.unified.count > 0 || stats.ultimate.count > 0) {
|
|
|
console.log('[API Resource Loader] Initialized successfully');
|
|
|
}
|
|
|
|
|
|
return this.resources;
|
|
|
} catch (error) {
|
|
|
|
|
|
this.initialized = true;
|
|
|
return this.resources;
|
|
|
} finally {
|
|
|
|
|
|
this.initPromise = null;
|
|
|
}
|
|
|
})();
|
|
|
|
|
|
return this.initPromise;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async loadResource(path) {
|
|
|
const cacheKey = `resource_${path}`;
|
|
|
|
|
|
|
|
|
if (this.cache.has(cacheKey)) {
|
|
|
return this.cache.get(cacheKey);
|
|
|
}
|
|
|
|
|
|
|
|
|
if (this.failedResources && this.failedResources.has(path)) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
let endpoint = null;
|
|
|
if (path.includes('crypto_resources_unified')) {
|
|
|
endpoint = '/api/resources/unified';
|
|
|
} else if (path.includes('ultimate_crypto_pipeline')) {
|
|
|
endpoint = '/api/resources/ultimate';
|
|
|
}
|
|
|
|
|
|
if (endpoint) {
|
|
|
try {
|
|
|
|
|
|
|
|
|
const controller = new AbortController();
|
|
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
|
|
|
|
let response = null;
|
|
|
try {
|
|
|
response = await fetch(endpoint, {
|
|
|
signal: controller.signal
|
|
|
});
|
|
|
} catch (fetchError) {
|
|
|
|
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
return null;
|
|
|
}
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
|
if (response && response.ok) {
|
|
|
try {
|
|
|
const result = await response.json();
|
|
|
if (result.success && result.data) {
|
|
|
this.cache.set(cacheKey, result.data);
|
|
|
return result.data;
|
|
|
}
|
|
|
} catch (jsonError) {
|
|
|
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
} catch (apiError) {
|
|
|
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const controller = new AbortController();
|
|
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
|
|
|
|
let response = null;
|
|
|
try {
|
|
|
response = await fetch(path, {
|
|
|
signal: controller.signal
|
|
|
});
|
|
|
} catch (fetchError) {
|
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
this.failedResources.add(path);
|
|
|
return null;
|
|
|
}
|
|
|
clearTimeout(timeoutId);
|
|
|
if (!response || !response.ok) {
|
|
|
|
|
|
if (response && response.status === 404) {
|
|
|
|
|
|
const altPaths = [
|
|
|
path.replace('/api-resources/', '/static/api-resources/'),
|
|
|
path.replace('/api-resources/', 'static/api-resources/'),
|
|
|
path.replace('/api-resources/', 'api-resources/')
|
|
|
];
|
|
|
|
|
|
for (const altPath of altPaths) {
|
|
|
try {
|
|
|
const altResponse = await fetch(altPath).catch(() => null);
|
|
|
if (altResponse && altResponse.ok) {
|
|
|
|
|
|
if (path.endsWith('.txt')) {
|
|
|
return await altResponse.text();
|
|
|
}
|
|
|
const data = await altResponse.json();
|
|
|
this.cache.set(cacheKey, data);
|
|
|
return data;
|
|
|
}
|
|
|
} catch (e) {
|
|
|
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
|
|
|
if (path.endsWith('.txt')) {
|
|
|
return await response.text();
|
|
|
}
|
|
|
|
|
|
const data = await response.json();
|
|
|
this.cache.set(cacheKey, data);
|
|
|
return data;
|
|
|
} catch (fileError) {
|
|
|
|
|
|
if (!path.startsWith('/static/') && !path.startsWith('static/')) {
|
|
|
try {
|
|
|
const staticPath = path.startsWith('/') ? `/static${path}` : `static/${path}`;
|
|
|
const controller2 = new AbortController();
|
|
|
const timeoutId2 = setTimeout(() => controller2.abort(), 5000);
|
|
|
const response = await fetch(staticPath, {
|
|
|
signal: controller2.signal
|
|
|
}).catch(() => null);
|
|
|
clearTimeout(timeoutId2);
|
|
|
|
|
|
if (response && response.ok) {
|
|
|
if (path.endsWith('.txt')) {
|
|
|
return await response.text();
|
|
|
}
|
|
|
const data = await response.json();
|
|
|
this.cache.set(cacheKey, data);
|
|
|
return data;
|
|
|
}
|
|
|
} catch (staticError) {
|
|
|
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
this.failedResources.add(path);
|
|
|
return null;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
|
|
|
this.failedResources.add(path);
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
parseConfigText(text) {
|
|
|
if (!text) return null;
|
|
|
|
|
|
|
|
|
const config = {};
|
|
|
const lines = text.split('\n');
|
|
|
|
|
|
for (const line of lines) {
|
|
|
const match = line.match(/^([^=]+)=(.*)$/);
|
|
|
if (match) {
|
|
|
config[match[1].trim()] = match[2].trim();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return config;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getMarketDataAPIs() {
|
|
|
const apis = [];
|
|
|
|
|
|
if (this.resources.unified?.registry?.market_data_apis) {
|
|
|
apis.push(...this.resources.unified.registry.market_data_apis);
|
|
|
}
|
|
|
|
|
|
if (this.resources.ultimate?.files?.[0]?.content?.resources) {
|
|
|
const marketAPIs = this.resources.ultimate.files[0].content.resources.filter(
|
|
|
r => r.category === 'Market Data'
|
|
|
);
|
|
|
apis.push(...marketAPIs.map(r => ({
|
|
|
id: r.name.toLowerCase().replace(/\s+/g, '_'),
|
|
|
name: r.name,
|
|
|
base_url: r.url,
|
|
|
auth: r.key ? { type: 'apiKeyQuery', key: r.key } : { type: 'none' },
|
|
|
rateLimit: r.rateLimit,
|
|
|
notes: r.desc
|
|
|
})));
|
|
|
}
|
|
|
|
|
|
return apis;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getNewsAPIs() {
|
|
|
const apis = [];
|
|
|
|
|
|
if (this.resources.unified?.registry?.news_apis) {
|
|
|
apis.push(...this.resources.unified.registry.news_apis);
|
|
|
}
|
|
|
|
|
|
if (this.resources.ultimate?.files?.[0]?.content?.resources) {
|
|
|
const newsAPIs = this.resources.ultimate.files[0].content.resources.filter(
|
|
|
r => r.category === 'News'
|
|
|
);
|
|
|
apis.push(...newsAPIs.map(r => ({
|
|
|
id: r.name.toLowerCase().replace(/\s+/g, '_'),
|
|
|
name: r.name,
|
|
|
base_url: r.url,
|
|
|
auth: r.key ? { type: 'apiKeyQuery', key: r.key } : { type: 'none' },
|
|
|
rateLimit: r.rateLimit,
|
|
|
notes: r.desc
|
|
|
})));
|
|
|
}
|
|
|
|
|
|
return apis;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getSentimentAPIs() {
|
|
|
const apis = [];
|
|
|
|
|
|
if (this.resources.unified?.registry?.sentiment_apis) {
|
|
|
apis.push(...this.resources.unified.registry.sentiment_apis);
|
|
|
}
|
|
|
|
|
|
if (this.resources.ultimate?.files?.[0]?.content?.resources) {
|
|
|
const sentimentAPIs = this.resources.ultimate.files[0].content.resources.filter(
|
|
|
r => r.category === 'Sentiment'
|
|
|
);
|
|
|
apis.push(...sentimentAPIs.map(r => ({
|
|
|
id: r.name.toLowerCase().replace(/\s+/g, '_'),
|
|
|
name: r.name,
|
|
|
base_url: r.url,
|
|
|
auth: r.key ? { type: 'apiKeyQuery', key: r.key } : { type: 'none' },
|
|
|
rateLimit: r.rateLimit,
|
|
|
notes: r.desc
|
|
|
})));
|
|
|
}
|
|
|
|
|
|
return apis;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getRPCNodes() {
|
|
|
if (this.resources.unified?.registry?.rpc_nodes) {
|
|
|
return this.resources.unified.registry.rpc_nodes;
|
|
|
}
|
|
|
return [];
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getBlockExplorers() {
|
|
|
if (this.resources.unified?.registry?.block_explorers) {
|
|
|
return this.resources.unified.registry.block_explorers;
|
|
|
}
|
|
|
return [];
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
searchAPIs(keyword) {
|
|
|
const results = [];
|
|
|
const lowerKeyword = keyword.toLowerCase();
|
|
|
|
|
|
|
|
|
if (this.resources.unified?.registry) {
|
|
|
const categories = ['market_data_apis', 'news_apis', 'sentiment_apis', 'rpc_nodes', 'block_explorers'];
|
|
|
for (const category of categories) {
|
|
|
const items = this.resources.unified.registry[category] || [];
|
|
|
for (const item of items) {
|
|
|
if (item.name?.toLowerCase().includes(lowerKeyword) ||
|
|
|
item.id?.toLowerCase().includes(lowerKeyword) ||
|
|
|
item.base_url?.toLowerCase().includes(lowerKeyword)) {
|
|
|
results.push({ ...item, category });
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if (this.resources.ultimate?.files?.[0]?.content?.resources) {
|
|
|
for (const resource of this.resources.ultimate.files[0].content.resources) {
|
|
|
if (resource.name?.toLowerCase().includes(lowerKeyword) ||
|
|
|
resource.desc?.toLowerCase().includes(lowerKeyword) ||
|
|
|
resource.url?.toLowerCase().includes(lowerKeyword)) {
|
|
|
results.push({
|
|
|
id: resource.name.toLowerCase().replace(/\s+/g, '_'),
|
|
|
name: resource.name,
|
|
|
base_url: resource.url,
|
|
|
category: resource.category,
|
|
|
auth: resource.key ? { type: 'apiKeyQuery', key: resource.key } : { type: 'none' },
|
|
|
rateLimit: resource.rateLimit,
|
|
|
notes: resource.desc
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return results;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getAPIById(id) {
|
|
|
|
|
|
if (this.resources.unified?.registry) {
|
|
|
const categories = ['market_data_apis', 'news_apis', 'sentiment_apis', 'rpc_nodes', 'block_explorers'];
|
|
|
for (const category of categories) {
|
|
|
const items = this.resources.unified.registry[category] || [];
|
|
|
const found = items.find(item => item.id === id);
|
|
|
if (found) return { ...found, category };
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if (this.resources.ultimate?.files?.[0]?.content?.resources) {
|
|
|
const found = this.resources.ultimate.files[0].content.resources.find(
|
|
|
r => r.name.toLowerCase().replace(/\s+/g, '_') === id
|
|
|
);
|
|
|
if (found) {
|
|
|
return {
|
|
|
id: found.name.toLowerCase().replace(/\s+/g, '_'),
|
|
|
name: found.name,
|
|
|
base_url: found.url,
|
|
|
category: found.category,
|
|
|
auth: found.key ? { type: 'apiKeyQuery', key: found.key } : { type: 'none' },
|
|
|
rateLimit: found.rateLimit,
|
|
|
notes: found.desc
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getStats() {
|
|
|
return {
|
|
|
unified: {
|
|
|
count: this.resources.unified?.registry?.metadata?.total_entries || 0,
|
|
|
market: this.resources.unified?.registry?.market_data_apis?.length || 0,
|
|
|
news: this.resources.unified?.registry?.news_apis?.length || 0,
|
|
|
sentiment: this.resources.unified?.registry?.sentiment_apis?.length || 0,
|
|
|
rpc: this.resources.unified?.registry?.rpc_nodes?.length || 0,
|
|
|
explorers: this.resources.unified?.registry?.block_explorers?.length || 0
|
|
|
},
|
|
|
ultimate: {
|
|
|
count: this.resources.ultimate?.total_sources || 0,
|
|
|
loaded: this.resources.ultimate?.files?.[0]?.content?.resources?.length || 0
|
|
|
},
|
|
|
initialized: this.initialized
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
window.apiResourceLoader = new APIResourceLoader();
|
|
|
|
|
|
|
|
|
if (document.readyState === 'loading') {
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
if (!window.apiResourceLoader.initialized && !window.apiResourceLoader.initPromise) {
|
|
|
window.apiResourceLoader.init().then(() => {
|
|
|
const stats = window.apiResourceLoader.getStats();
|
|
|
if (stats.unified.count > 0 || stats.ultimate.count > 0) {
|
|
|
console.log('[API Resource Loader] Ready!', stats);
|
|
|
}
|
|
|
}).catch(() => {
|
|
|
|
|
|
});
|
|
|
}
|
|
|
}, { once: true });
|
|
|
} else {
|
|
|
if (!window.apiResourceLoader.initialized && !window.apiResourceLoader.initPromise) {
|
|
|
window.apiResourceLoader.init().then(() => {
|
|
|
const stats = window.apiResourceLoader.getStats();
|
|
|
if (stats.unified.count > 0 || stats.ultimate.count > 0) {
|
|
|
console.log('[API Resource Loader] Ready!', stats);
|
|
|
}
|
|
|
}).catch(() => {
|
|
|
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|