Fetch miner level data from ronencoin.tech and inject levels on OpenSea with cache and refresh button
// ==UserScript==
// @name Fetch NFT Miner Levels from ronencoin.tech (with Cache & Refresh)
// @namespace https://opensea.io/collection/ronen-coin-mining-network/
// @version 1.2
// @author spyderbibek
// @description Fetch miner level data from ronencoin.tech and inject levels on OpenSea with cache and refresh button
// @match https://opensea.io/collection/ronen-coin-mining-network*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @connect ronencoin.tech
// @license MIT
// ==/UserScript==
(async function() {
'use strict';
// === CONFIGURATION ===
const DEBUG = false; // Set to false to disable debug logs
const CACHE_KEY = 'ronen_token_levels_cache';
const CACHE_DATE_KEY = 'ronen_cache_last_refresh';
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
// === UTILS ===
function debugLog(...args) {
if (DEBUG) console.log('[NFTLevelInjector]', ...args);
}
function formatDate(ts) {
const d = new Date(ts);
return d.toLocaleString();
}
// Delay helper
const delay = ms => new Promise(res => setTimeout(res, ms));
// Cache helpers using GM storage
async function loadCache() {
const raw = await GM_getValue(CACHE_KEY, '{}');
try {
return JSON.parse(raw);
} catch {
return {};
}
}
async function saveCache(cache) {
await GM_setValue(CACHE_KEY, JSON.stringify(cache));
}
async function clearCache() {
await GM_deleteValue(CACHE_KEY);
await GM_setValue(CACHE_DATE_KEY, Date.now().toString());
debugLog('Cache cleared');
updateRefreshLabel();
}
async function getLastRefreshTime() {
const last = await GM_getValue(CACHE_DATE_KEY, '0');
return parseInt(last) || 0;
}
async function setLastRefreshTime(ts) {
await GM_setValue(CACHE_DATE_KEY, ts.toString());
}
async function isCacheExpired() {
const last = await getLastRefreshTime();
return (Date.now() - last) > CACHE_TTL_MS;
}
// === DOM Helpers ===
function findTokenElements() {
return Array.from(document.querySelectorAll('a[href*="/item/ronin/"]'));
}
function getTokenIdFromElement(el) {
try {
const urlParts = el.href.split('/');
return urlParts[urlParts.length - 1];
} catch {
return null;
}
}
// === Fetch using GM_xmlhttpRequest with Promise wrapper ===
function gmFetch(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
headers: {
'Accept': 'text/html',
},
onload: res => {
if (res.status >= 200 && res.status < 300) {
resolve(res.responseText);
} else {
reject(new Error(`HTTP status ${res.status}`));
}
},
onerror: err => reject(err),
});
});
}
// === Fetch level info from ronencoin.tech with exact token matching ===
async function fetchLevel(tokenId) {
debugLog(`Fetching level for token: ${tokenId}`);
try {
const url = `https://ronencoin.tech/nft_miners.php?search_token=${tokenId}&type=&level=`;
const text = await gmFetch(url);
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const rows = doc.querySelectorAll('table tbody tr');
if (!rows.length) {
debugLog(`No data rows found for token ${tokenId}`);
return null;
}
// Find exact token match row
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length < 4) continue;
const tokenFromTable = cells[0].textContent.trim();
if (tokenFromTable === tokenId) {
const level = cells[3].textContent.trim();
debugLog(`Found exact match level ${level} for token ${tokenId}`);
return level;
}
}
debugLog(`No exact match found for token ${tokenId}`);
return null;
} catch (e) {
console.error(`[NFTLevelInjector] Error fetching level for token ${tokenId}:`, e);
return null;
}
}
// === Inject level into page ===
function injectLevel(tokenId, level) {
const els = findTokenElements().filter(el => getTokenIdFromElement(el) === tokenId);
if (els.length === 0) {
debugLog(`No element found to inject level for token ${tokenId}`);
return;
}
els.forEach(el => {
if (el.querySelector('.miner-level')) {
debugLog(`Level already injected for token ${tokenId}`);
return;
}
const span = document.createElement('span');
span.className = 'miner-level';
span.style.color = '#facc15';
span.style.marginLeft = '6px';
span.style.fontWeight = 'bold';
span.textContent = `(Lvl: ${level})`;
el.appendChild(span);
debugLog(`Injected level for token ${tokenId}`);
});
}
// === Main processing of tokens, with caching ===
async function processTokens(tokenEls, cache) {
for (const el of tokenEls) {
const tokenId = getTokenIdFromElement(el);
if (!tokenId) continue;
if (processedTokens.has(tokenId)) {
// Already processed this token this session
continue;
}
processedTokens.add(tokenId);
if (cache[tokenId] !== undefined) {
// Use cached data
injectLevel(tokenId, cache[tokenId]);
debugLog(`Used cached level for token ${tokenId}: ${cache[tokenId]}`);
continue;
}
// Fetch fresh data and update cache
const level = await fetchLevel(tokenId);
if (level !== null) {
cache[tokenId] = level;
injectLevel(tokenId, level);
debugLog(`Fetched and cached level for token ${tokenId}: ${level}`);
}
// Delay between requests
await delay(800);
}
await saveCache(cache);
}
// === UI overlay with refresh button and label ===
function createOverlay() {
const container = document.createElement('div');
container.style.position = 'fixed';
container.style.bottom = '10px';
container.style.left = '10px';
container.style.background = 'rgba(0,0,0,0.7)';
container.style.color = '#facc15';
container.style.padding = '8px 12px';
container.style.borderRadius = '6px';
container.style.fontSize = '14px';
container.style.zIndex = 999999;
container.style.fontFamily = 'Arial, sans-serif';
container.style.userSelect = 'none';
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.gap = '10px';
const btn = document.createElement('button');
btn.textContent = 'Refresh Data';
btn.style.cursor = 'pointer';
btn.style.background = '#facc15';
btn.style.border = 'none';
btn.style.borderRadius = '4px';
btn.style.color = '#000';
btn.style.fontWeight = 'bold';
btn.style.padding = '4px 8px';
btn.style.userSelect = 'none';
const label = document.createElement('div');
label.style.minWidth = '150px';
label.textContent = 'Last refreshed: N/A';
btn.onclick = async () => {
btn.disabled = true;
btn.textContent = 'Refreshing...';
await clearCache();
await main(true); // force reload data
btn.textContent = 'Refresh Data';
btn.disabled = false;
};
container.appendChild(btn);
container.appendChild(label);
document.body.appendChild(container);
return label;
}
// Update the "Last Refreshed" label
async function updateRefreshLabel() {
const lastRefresh = await getLastRefreshTime();
if (!refreshLabel) return;
refreshLabel.textContent = `Last refreshed: ${lastRefresh ? formatDate(lastRefresh) : 'Never'}`;
}
// === Globals ===
let processedTokens = new Set();
let refreshLabel = null;
// === Main ===
async function main(forceRefresh = false) {
processedTokens = new Set();
let cache = await loadCache();
// If cache expired or forceRefresh, clear it to force refetch
if (forceRefresh || await isCacheExpired()) {
debugLog('Cache expired or forced refresh, clearing cache...');
await clearCache();
cache = {};
await setLastRefreshTime(Date.now());
}
updateRefreshLabel();
const tokens = findTokenElements();
debugLog(`Initial tokens found: ${tokens.length}`);
await processTokens(tokens, cache);
// Set last refresh time if we didn't just clear it
if (!forceRefresh) {
await setLastRefreshTime(Date.now());
updateRefreshLabel();
}
}
// Start the overlay UI
refreshLabel = createOverlay();
// Initial run
await main();
// Watch for new tokens dynamically added to page
const observer = new MutationObserver(async mutations => {
const newTokens = [];
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.matches && node.matches('a[href*="/item/ronin/"]')) {
newTokens.push(node);
}
newTokens.push(...(node.querySelectorAll ? Array.from(node.querySelectorAll('a[href*="/item/ronin/"]')) : []));
});
});
if (newTokens.length > 0) {
debugLog(`Detected ${newTokens.length} new token elements`);
const cache = await loadCache();
await processTokens(newTokens, cache);
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();