// ==UserScript==
// @name GMGN Fee Unlock
// @namespace https://greasyfork.org/zh-CN/scripts/547048-gmgn-fee-unlock
// @version 1.0.2
// @description GMGN解除sol链gas最低0.0001的限制,悬浮图标常驻,点击打开/关闭面板
// @match https://gmgn.ai/*
// @run-at document-idle
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = 'GMGN_FEE_SETTINGS';
const DRAG_TAB_KEY = 'GMGN_FEE_TAB_POS';
const DRAG_PANEL_KEY = 'GMGN_FEE_PANEL_POS';
const defaultState = {
enabled: true,
feePriority: '0.00000001',
feeBribe: '0.00000001',
hidden: false,
dragLocked: true
};
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
const state = { ...defaultState, ...stored };
const log = (...a) => console.log('%c[GMGN-FEE]', 'color:#7ad39a;font-weight:700', ...a);
function saveState() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); }
function normalizeText(s) { return (s || '').replace(/\s+/g, ' ').toLowerCase(); }
// —— 新增:判断是否是我们自己面板的控件 —— //
function isOurPanelInput(input) {
if (!input) return false;
if (input.id === 'fee-priority' || input.id === 'fee-bribe') return true;
if (input.closest && input.closest('#gmgn-fee-panel')) return true;
if (input.closest && input.closest('#fee-tab')) return true;
return false;
}
function detectFeeType(input) {
// 根据附近文本区分优先费 / 贿赂费(只用于页面上的真实输入框)
let cur = input;
for (let i = 0; i < 6 && cur; i++, cur = cur.parentElement) {
const txt = normalizeText(cur.textContent || '');
if (!txt) continue;
if (txt.includes('priority') || txt.includes('优先')) return 'priority';
if (txt.includes('bribe') || txt.includes('贿赂')) return 'bribe';
}
return null;
}
function getWantFor(input) {
const type = detectFeeType(input);
if (type === 'priority') return state.feePriority;
if (type === 'bribe') return state.feeBribe;
return state.feePriority; // 默认走优先费
}
const patched = new WeakSet();
function patchFeeInput(input) {
if (!input || patched.has(input)) return;
// —— 关键修复:忽略我们自己的面板输入框 —— //
if (isOurPanelInput(input)) return;
const type = detectFeeType(input);
if (!type) return;
try {
input.removeAttribute('min');
input.setAttribute('step', 'any');
input.setAttribute('inputmode', 'decimal');
input.setAttribute('pattern', '[0-9]*(\\.[0-9]+)?');
input.setAttribute('aria-valuemin', '0');
input.closest('.chakra-numberinput')?.setAttribute('data-tm-fee', '1');
function enforce() {
if (!state.enabled) return;
try {
const want = getWantFor(input);
const now = input.value || input.getAttribute('aria-valuenow') || '';
const nowN = parseFloat(String(now || '0'));
const wantN = parseFloat(String(want));
if (isNaN(nowN) || nowN === 0 || (nowN >= 0.0001 && wantN < 0.0001) || nowN !== wantN) {
const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
if (desc && desc.set) { try { desc.set.call(input, want); } catch (e) {} }
else input.value = want;
input.setAttribute('aria-valuenow', want);
input.setAttribute('aria-valuetext', want);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
const wrap = input.closest('.chakra-numberinput');
if (wrap) wrap.setAttribute('value', want);
log(`enforce ${type} →`, want);
}
} catch (e) {}
}
const handler = () => { if (!state.enabled) return setTimeout(enforce, 8); };
input.addEventListener('input', handler, true);
input.addEventListener('change', handler, true);
input.addEventListener('blur', handler, true);
const attrObs = new MutationObserver(() => { if (state.enabled) setTimeout(enforce, 6); });
attrObs.observe(input, { attributes: true, attributeFilter: ['aria-valuemin', 'value', 'aria-valuenow', 'aria-valuetext'] });
enforce();
patched.add(input);
input.__feeFix__attrObserver = attrObs;
} catch (e) { console.warn('[GMGN-FEE] patch error', e); }
}
function scanAndPatch(root = document) {
try {
const inputs = Array.from((root || document).querySelectorAll('input'));
for (const inp of inputs) {
if (isOurPanelInput(inp)) continue; // —— 关键修复:扫描阶段也忽略面板输入 —— //
patchFeeInput(inp);
}
} catch (e) { console.warn('[GMGN-FEE] scan error', e); }
}
function autoApply(root = document) { scanAndPatch(root); }
const mo = new MutationObserver(muts => {
if (!state.enabled) return;
for (const m of muts) {
if (m.type === 'childList' && m.addedNodes.length) {
for (const n of m.addedNodes) if (n.nodeType === 1) autoApply(n);
} else if (m.type === 'attributes' && m.target instanceof HTMLInputElement) {
if (isOurPanelInput(m.target)) continue; // —— 关键修复:忽略面板输入 —— //
patchFeeInput(m.target);
}
}
});
function isInteractiveElement(el) {
if (!el) return false;
const tag = el.tagName && el.tagName.toLowerCase();
if (!tag) return false;
if (['input', 'button', 'select', 'textarea', 'label', 'a'].includes(tag)) return true;
if (el.closest && el.closest('input,button,select,textarea,a,label')) return true;
return false;
}
function mountPanel() {
if (document.getElementById('gmgn-fee-panel')) return;
const wrap = document.createElement('div');
wrap.id = 'gmgn-fee-panel';
Object.assign(wrap.style, {
position: 'fixed',
right: '14px',
bottom: '80px',
zIndex: 99999999,
background: '#0f1115',
color: '#e6f3ea',
padding: '10px',
borderRadius: '10px',
border: '1px solid rgba(255,255,255,0.04)',
fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, Arial',
fontSize: '13px',
width: '15%',
minWidth: '165px',
maxWidth: '240px',
boxShadow: '0 6px 18px rgba(0,0,0,.4)',
display: state.hidden ? 'none' : 'block',
transform: 'none'
});
wrap.innerHTML = `
<div style="font-weight:700;margin-bottom:8px;">Fee Unlock</div>
<div style="display:flex;align-items:center;gap:16px;margin-bottom:8px;">
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
<input id="fee-enabled" type="checkbox" ${state.enabled ? 'checked' : ''}>
<span style="font-size:12px">自动保持(防回弹)</span>
</label>
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
<input id="drag-locked" type="checkbox" ${state.dragLocked ? 'checked' : ''}>
<span style="font-size:12px">拖动锁定</span>
</label>
</div>
<div style="display:grid;grid-template-columns:64px 1fr;gap:6px;align-items:center;margin-bottom:8px;">
<div style="font-size:12px">优先费</div>
<input id="fee-priority" value="${state.feePriority}" style="padding:6px;border-radius:6px;background:#151821;border:1px solid #333;color:#e6f3ea;width:100%;box-sizing:border-box;">
<div style="font-size:12px">贿赂费</div>
<input id="fee-bribe" value="${state.feeBribe}" style="padding:6px;border-radius:6px;background:#151821;border:1px solid #333;color:#e6f3ea;width:100%;box-sizing:border-box;">
</div>
<div style="display:flex;gap:8px;">
<button id="fee-apply" style="flex:1;padding:8px;border-radius:8px;background:#81D69D;color:white;border:none;cursor:pointer">立即应用</button>
<button id="fee-hide" style="padding:8px;border-radius:8px;background:#242630;color:#e6f3ea;border:none;cursor:pointer">隐藏</button>
</div>
`;
// 悬浮 tab
const tab = document.createElement('div');
tab.id = 'fee-tab';
tab.textContent = 'Fee Unlock';
Object.assign(tab.style, {
position: 'fixed',
zIndex: 99999999,
background: '#21302D',
color: '#81D69D',
padding: '3.25px 5px',
borderRadius: '6px',
fontSize: '12px',
cursor: 'pointer',
display: 'block'
});
const savedTabPos = JSON.parse(localStorage.getItem(DRAG_TAB_KEY) || '{}');
if (savedTabPos.right) tab.style.right = savedTabPos.right;
else tab.style.right = '20px';
if (savedTabPos.bottom) tab.style.bottom = savedTabPos.bottom;
else tab.style.bottom = '20px';
// load saved panel ratio position
const savedPanelPos = JSON.parse(localStorage.getItem(DRAG_PANEL_KEY) || '{}');
if (savedPanelPos.ratioLeft && savedPanelPos.ratioTop) {
wrap.dataset.ratioLeft = savedPanelPos.ratioLeft;
wrap.dataset.ratioTop = savedPanelPos.ratioTop;
wrap.style.left = (window.innerWidth * parseFloat(wrap.dataset.ratioLeft)) + 'px';
wrap.style.top = (window.innerHeight * parseFloat(wrap.dataset.ratioTop)) + 'px';
wrap.style.right = 'auto';
wrap.style.bottom = 'auto';
} else {
wrap.style.right = '14px';
wrap.style.bottom = '80px';
wrap.style.left = '';
wrap.style.top = '';
}
document.body.appendChild(wrap);
document.body.appendChild(tab);
const elEnabled = wrap.querySelector('#fee-enabled');
const elPriority = wrap.querySelector('#fee-priority');
const elBribe = wrap.querySelector('#fee-bribe');
const elApply = wrap.querySelector('#fee-apply');
const elHide = wrap.querySelector('#fee-hide');
const elDragLocked = wrap.querySelector('#drag-locked');
// small CSS for checkbox visuals
const style = document.createElement('style');
style.innerHTML = `input[type=checkbox] { accent-color: #ffffff; width: 14px; height: 14px; }`;
document.head.appendChild(style);
elEnabled.addEventListener('change', e => { state.enabled = !!e.target.checked; saveState(); if (state.enabled) autoApply(); });
// 单独保存,不相互覆盖
elPriority.addEventListener('input', e => { state.feePriority = e.target.value; saveState(); });
elBribe.addEventListener('input', e => { state.feeBribe = e.target.value; saveState(); });
elApply.addEventListener('click', () => autoApply());
elHide.addEventListener('click', () => {
wrap.style.display = 'none';
tab.style.display = 'block';
state.hidden = true;
saveState();
});
tab.addEventListener('click', (ev) => {
// If tab was just dragged, tabMoved logic will prevent toggle (implemented below)
if (tab._moved) { tab._moved = false; ev.stopImmediatePropagation(); return; }
wrap.style.display = wrap.style.display === 'none' ? 'block' : 'none';
state.hidden = wrap.style.display === 'none';
saveState();
});
elDragLocked.addEventListener('change', e => { state.dragLocked = !!e.target.checked; saveState(); });
// ---------- Dragging logic ----------
let dragPanel = false, oxPanel = 0, oyPanel = 0;
let panelMoved = false;
let startX = 0, startY = 0;
function isInteractiveElement(el) {
if (!el) return false;
const tag = el.tagName && el.tagName.toLowerCase();
if (!tag) return false;
if (['input', 'button', 'select', 'textarea', 'label', 'a'].includes(tag)) return true;
if (el.closest && el.closest('input,button,select,textarea,a,label')) return true;
return false;
}
wrap.addEventListener('mousedown', (ev) => {
if (state.dragLocked) return;
if (isInteractiveElement(ev.target)) return;
const rect = wrap.getBoundingClientRect();
if (!wrap.style.left || wrap.style.left === '') {
wrap.style.left = rect.left + 'px';
}
if (!wrap.style.top || wrap.style.top === '') {
wrap.style.top = rect.top + 'px';
}
wrap.style.right = 'auto';
wrap.style.bottom = 'auto';
dragPanel = true;
oxPanel = ev.clientX - parseFloat(wrap.style.left || rect.left);
oyPanel = ev.clientY - parseFloat(wrap.style.top || rect.top);
panelMoved = false;
startX = ev.clientX;
startY = ev.clientY;
wrap.style.cursor = 'grabbing';
ev.preventDefault();
});
function onDocumentMouseMoveForPanel(ev) {
if (!dragPanel) return;
const newLeft = ev.clientX - oxPanel;
const newTop = ev.clientY - oyPanel;
const clampedLeft = Math.max(0, Math.min(window.innerWidth - wrap.offsetWidth, newLeft));
const clampedTop = Math.max(0, Math.min(window.innerHeight - wrap.offsetHeight, newTop));
wrap.style.left = clampedLeft + 'px';
wrap.style.top = clampedTop + 'px';
wrap.style.right = 'auto';
wrap.style.bottom = 'auto';
if (!panelMoved && (Math.abs(ev.clientX - startX) > 3 || Math.abs(ev.clientY - startY) > 3)) panelMoved = true;
}
function onDocumentMouseUpForPanel() {
if (!dragPanel) return;
dragPanel = false;
wrap.style.cursor = 'default';
const rect = wrap.getBoundingClientRect();
wrap.dataset.ratioLeft = (rect.left / window.innerWidth).toString();
wrap.dataset.ratioTop = (rect.top / window.innerHeight).toString();
localStorage.setItem(DRAG_PANEL_KEY, JSON.stringify({ ratioLeft: wrap.dataset.ratioLeft, ratioTop: wrap.dataset.ratioTop }));
}
document.addEventListener('mousemove', onDocumentMouseMoveForPanel);
document.addEventListener('mouseup', onDocumentMouseUpForPanel);
wrap.addEventListener('click', (ev) => {
if (panelMoved) {
ev.stopImmediatePropagation();
ev.preventDefault();
panelMoved = false;
}
}, true);
// ---------- Tab dragging ----------
let dragTab = false, oxTab = 0, oyTab = 0, tabMoved = false;
tab.addEventListener('mousedown', (ev) => {
if (state.dragLocked) return;
dragTab = true;
oxTab = ev.clientX - (tab.getBoundingClientRect().left);
oyTab = ev.clientY - (tab.getBoundingClientRect().top);
tab.style.cursor = 'grabbing';
tabMoved = false;
tab._startX = ev.clientX;
tab._startY = ev.clientY;
ev.preventDefault();
});
function onDocumentMouseMoveForTab(ev) {
if (!dragTab) return;
const newLeft = ev.clientX - oxTab;
const newTop = ev.clientY - oyTab;
const right = Math.max(0, Math.min(window.innerWidth - tab.offsetWidth, window.innerWidth - (newLeft + tab.offsetWidth)));
const bottom = Math.max(0, Math.min(window.innerHeight - tab.offsetHeight, window.innerHeight - (newTop + tab.offsetHeight)));
tab.style.right = right + 'px';
tab.style.bottom = bottom + 'px';
tab.style.left = 'auto';
tab.style.top = 'auto';
if (!tabMoved && (Math.abs(ev.clientX - tab._startX) > 3 || Math.abs(ev.clientY - tab._startY) > 3)) tabMoved = true;
if (tabMoved) tab._moved = true;
}
function onDocumentMouseUpForTab() {
if (!dragTab) return;
dragTab = false;
tab.style.cursor = 'pointer';
localStorage.setItem(DRAG_TAB_KEY, JSON.stringify({ right: tab.style.right, bottom: tab.style.bottom }));
setTimeout(() => { tab._moved = false; }, 300);
}
document.addEventListener('mousemove', onDocumentMouseMoveForTab);
document.addEventListener('mouseup', onDocumentMouseUpForTab);
tab.addEventListener('click', function (ev) {
if (tabMoved || this._moved) {
ev.stopImmediatePropagation();
ev.preventDefault();
tabMoved = false;
this._moved = false;
}
}, true);
function initPanelPosition() {
const p = JSON.parse(localStorage.getItem(DRAG_PANEL_KEY) || '{}');
if (p.ratioLeft && p.ratioTop) {
const left = Math.round(window.innerWidth * parseFloat(p.ratioLeft));
const top = Math.round(window.innerHeight * parseFloat(p.ratioTop));
wrap.style.left = left + 'px';
wrap.style.top = top + 'px';
wrap.style.right = 'auto';
wrap.style.bottom = 'auto';
wrap.dataset.ratioLeft = p.ratioLeft;
wrap.dataset.ratioTop = p.ratioTop;
} else {
wrap.style.left = '';
wrap.style.top = '';
wrap.style.right = '14px';
wrap.style.bottom = '80px';
delete wrap.dataset.ratioLeft;
delete wrap.dataset.ratioTop;
}
}
initPanelPosition();
window.addEventListener('resize', () => {
if (wrap.dataset.ratioLeft && wrap.dataset.ratioTop) {
wrap.style.left = Math.round(window.innerWidth * parseFloat(wrap.dataset.ratioLeft)) + 'px';
wrap.style.top = Math.round(window.innerHeight * parseFloat(wrap.dataset.ratioTop)) + 'px';
wrap.style.right = 'auto';
wrap.style.bottom = 'auto';
}
});
requestAnimationFrame(() => {
wrap.style.willChange = 'opacity';
void wrap.getBoundingClientRect();
wrap.style.willChange = '';
});
}
function start() {
setTimeout(() => {
try { mountPanel(); } catch (e) { console.warn(e); }
autoApply(document);
try {
mo.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['aria-valuemin', 'aria-valuenow', 'value', 'min']
});
} catch (e) { console.warn('[GMGN-FEE] mo start error', e); }
setInterval(() => { if (state.enabled) scanAndPatch(document); }, 1000);
}, 2000);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
else start();
})();