// ==UserScript==
// @name HomeBase AutoVault
// @namespace https://github.com/ZerocuCodes
// @version 1.0.0
// @description Automatically vaults a percentage of your profits on degen.com into the site's built-in vault
// @author Zerocu
// @license MIT
// @homepageURL https://github.com/ZerocuCodes/Homebase-AutoVault-for-Degen.com
// @supportURL https://github.com/ZerocuCodes/Homebase-AutoVault-for-Degen.com/issues
// @updateURL https://raw.githubusercontent.com/ZerocuCodes/Homebase-AutoVault-for-Degen.com/main/degenautovault.user.js
// @downloadURL https://raw.githubusercontent.com/ZerocuCodes/Homebase-AutoVault-for-Degen.com/main/degenautovault.user.js
// @match https://degen.com/*
// @match https://*.degen.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=degen.com
// @run-at document-idle
// @grant none
// ==/UserScript==
(function() {
'use strict';
// --- Config ---
const INIT_DELAY = 2000;
const RATE_LIMIT_MAX = 50;
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour
const API_BASE = 'https://api.degen.com/v1';
const TRACKED_ASSET_KEY = 'degenvault-tracked-asset';
function loadConfig() {
const saved = localStorage.getItem('degenvault-config');
if (saved) {
try {
return JSON.parse(saved);
} catch (e) {
log('Failed to load saved config:', e);
}
}
return {
saveAmount: 0.1,
bigWinThreshold: 5,
bigWinMultiplier: 3,
checkInterval: 90000
};
}
function saveConfig(config) {
localStorage.setItem('degenvault-config', JSON.stringify(config));
}
function loadTrackedAsset() {
return localStorage.getItem(TRACKED_ASSET_KEY) || null;
}
function saveTrackedAsset(asset) {
localStorage.setItem(TRACKED_ASSET_KEY, asset);
}
let config = loadConfig();
let ASSET = loadTrackedAsset();
let SAVE_AMOUNT = config.saveAmount;
let BIG_WIN_THRESHOLD = config.bigWinThreshold;
let BIG_WIN_MULTIPLIER = config.bigWinMultiplier;
let CHECK_INTERVAL = config.checkInterval;
let isScriptInitialized = false;
// --- Activity Log ---
const activityLog = [];
const MAX_LOG_ENTRIES = 50;
let onLogUpdate = null;
function logActivity(message, type = 'info') {
const entry = {
time: new Date(),
message,
type // 'info', 'success', 'warning', 'profit', 'bigwin', 'error'
};
activityLog.unshift(entry);
if (activityLog.length > MAX_LOG_ENTRIES) activityLog.pop();
console.log('[DegenVault]', message);
if (onLogUpdate) onLogUpdate(entry);
}
const log = (...args) => logActivity(args.join(' '), 'info');
// --- Flavor Text (Degen-themed) ---
const FLAVOR = {
profit: [
"Profit spotted, ape'd it to the vault",
"Green candle, stacking the bag"
],
bigWin: [
"Big W detected, diamond-handing this one",
"Massive candle, locking in the bag"
],
deposit: [
"Deposit detected",
],
start: [
"DegenVault online, no paper hands here",
"Watching the wallet, ready to ape profits into the vault"
],
stop: [
"DegenVault paused",
"Monitoring stopped"
],
rateLimit: [
"Rate limited, vaulting paused. Chill until it resets",
"Limit reached, vaulting paused. Touch grass until it resets"
]
};
const pickFlavor = (arr) => arr[Math.floor(Math.random() * arr.length)];
// --- Degen API ---
class DegenApi {
constructor() {
this.apiBase = API_BASE;
}
async _fetch(path, opts) {
try {
const res = await fetch(this.apiBase + path, {
credentials: 'include',
headers: { 'content-type': 'application/json' },
mode: 'cors',
cache: 'no-cache',
...opts
});
const rateRemaining = res.headers.get('x-ratelimit-remaining');
const rateReset = res.headers.get('x-ratelimit-reset');
if (rateRemaining !== null) {
apiRateInfo.remaining = parseInt(rateRemaining, 10);
apiRateInfo.resetMs = parseInt(rateReset, 10) || 0;
}
if (!res.ok) {
log(`API call failed with status ${res.status}: ${res.statusText}`);
return { error: true, status: res.status, message: res.statusText };
}
if (res.status === 204) return {};
return res.json();
} catch (e) {
log('API call failed:', e.message);
return { error: true, message: e.message, type: 'network' };
}
}
async getVaults() {
return this._fetch('/balance/vaults', { method: 'GET' });
}
// Read-only: every asset's wallet balance plus which one is primary.
async getWallets() {
return this._fetch('/balance/wallets', { method: 'GET' });
}
async depositToVault(asset, amount) {
const body = JSON.stringify({
operationId: crypto.randomUUID(),
asset,
amount: String(amount)
});
return this._fetch('/balance/vault/deposit', { method: 'POST', body });
}
// Read-only: the current account's profile β source of the user id the
// statistics endpoints are keyed by.
async getProfile() {
return this._fetch('/users/profile', { method: 'GET' });
}
// Read-only: public user object; .statistics.wagered is Degen's
// server-side lifetime USD-equivalent wagered counter.
async getPublicUser(id) {
return this._fetch(`/users/public/${encodeURIComponent(id)}`, { method: 'GET' });
}
async getUserStats(id) {
return this._fetch(`/users/${encodeURIComponent(id)}/stats`, { method: 'GET' });
}
}
let apiRateInfo = { remaining: null, resetMs: null };
function setTrackedAsset(asset) {
if (!asset) return;
const upper = asset.toUpperCase();
if (upper === ASSET) return;
const first = !ASSET;
ASSET = upper;
saveTrackedAsset(ASSET);
if (uiWidget) uiWidget.updateTrackedAsset(ASSET);
if (vaultDisplay) vaultDisplay.setAsset(ASSET);
logActivity(first ? `π‘ Detected active currency: ${ASSET}` : `π± Currency on Degen switched β now tracking ${ASSET}`, 'info');
if (running) isInitialized = false;
refreshApiVaultBalance();
}
// --- Vault Display UI (floaty) ---
class VaultDisplay {
constructor() {
this._el = document.createElement("span");
this._vaulted = 0;
this._apiVaultBalance = 0;
this._asset = ASSET;
this._load();
this.render();
}
_storageKey() {
return `degenvault-vaulted-session:${(this._asset || '').toUpperCase()}`;
}
_load() {
try {
const raw = sessionStorage.getItem(this._storageKey());
const v = parseFloat(raw);
if (!isNaN(v) && v >= 0) this._vaulted = v;
} catch (e) {}
}
_save() {
try {
sessionStorage.setItem(this._storageKey(), String(this._vaulted));
} catch (e) {}
}
setAsset(asset) {
this._asset = (asset || ASSET || '').toUpperCase();
this._load();
this.render();
}
setApiVaultBalance(amount) {
const n = parseFloat(amount);
if (!isNaN(n)) this._apiVaultBalance = n;
this.render();
}
render() {
if (!this._el) return;
this._el.innerText = (this._apiVaultBalance || this._vaulted || 0).toFixed(8);
}
update(amount) {
const add = +amount;
if (isNaN(add) || add <= 0) return;
this._vaulted = (this._vaulted || 0) + add;
this._save();
this.render();
}
reset() {
this._vaulted = 0;
this._save();
this.render();
}
}
// --- Rate Limit Tracking (client-side, in addition to API headers) ---
function loadRateLimitData() {
const saved = sessionStorage.getItem('degenvault-ratelimit');
if (saved) {
try {
const data = JSON.parse(saved);
return data.filter(ts => Date.now() - ts < RATE_LIMIT_WINDOW);
} catch (e) {
log('Failed to load rate limit data:', e);
}
}
return [];
}
function saveRateLimitData(timestamps) {
sessionStorage.setItem('degenvault-ratelimit', JSON.stringify(timestamps));
}
let vaultActionTimestamps = loadRateLimitData();
function canVaultNow() {
const now = Date.now();
vaultActionTimestamps = vaultActionTimestamps.filter(ts => now - ts < RATE_LIMIT_WINDOW);
saveRateLimitData(vaultActionTimestamps);
if (apiRateInfo.remaining !== null && apiRateInfo.remaining <= 1) return false;
return vaultActionTimestamps.length < RATE_LIMIT_MAX;
}
function getVaultCountLastHour() {
const now = Date.now();
vaultActionTimestamps = vaultActionTimestamps.filter(ts => now - ts < RATE_LIMIT_WINDOW);
return vaultActionTimestamps.length;
}
function initHelpTooltips(widget) {
let tooltipEl = document.getElementById('degenvault-tooltip');
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl.id = 'degenvault-tooltip';
document.body.appendChild(tooltipEl);
}
function positionTooltip(anchor) {
const r = anchor.getBoundingClientRect();
tooltipEl.style.left = '0px';
tooltipEl.style.top = '0px';
const tw = tooltipEl.offsetWidth;
const th = tooltipEl.offsetHeight;
let left = r.right - tw;
let top = r.top - th - 8;
if (top < 4) top = r.bottom + 8;
if (left < 4) left = 4;
if (left + tw > window.innerWidth - 4) left = window.innerWidth - tw - 4;
tooltipEl.style.left = `${left}px`;
tooltipEl.style.top = `${top}px`;
}
widget.querySelectorAll('.dv-help').forEach(el => {
el.addEventListener('mouseenter', () => {
tooltipEl.textContent = el.getAttribute('data-tip') || '';
tooltipEl.classList.add('visible');
positionTooltip(el);
});
el.addEventListener('mouseleave', () => {
tooltipEl.classList.remove('visible');
});
});
}
// --- Floaty UI Widget ---
let currentViewMode = 'full';
function createVaultFloatyUI(startCallback, stopCallback, getParams, setParams, vaultDisplay, syncCallback, fetchWalletsCallback, manualVaultCallback) {
if (document.getElementById('degenvault-floaty')) document.getElementById('degenvault-floaty').remove();
if (document.getElementById('degenvault-stealth')) document.getElementById('degenvault-stealth').remove();
const style = document.createElement('style');
style.id = 'degenvault-styles';
style.textContent = `
#degenvault-floaty {
--hb-bg: #000000;
--hb-panel: #060907;
--hb-panel-2: #0b100c;
--hb-border: #163019;
--hb-accent: #00FF41;
--hb-accent-dark: #00b82e;
--hb-text: #cdf5cf;
--hb-text-dim: #5f8a63;
background: var(--hb-bg);
color: var(--hb-text);
border: 1px solid var(--hb-border);
border-radius: 4px;
box-shadow: 0 4px 28px rgba(0,0,0,0.6), 0 0 22px rgba(0,255,65,0.08);
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 13px;
min-width: 250px;
max-width: 290px;
user-select: none;
position: fixed;
top: 80px;
right: 20px;
z-index: 999999;
display: flex;
flex-direction: column;
clip-path: polygon(0 10px, 10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%);
}
#degenvault-floaty.hidden { display: none; }
#degenvault-floaty .dv-header {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--hb-panel-2);
padding: 9px 10px;
border-bottom: 1px solid var(--hb-border);
cursor: grab;
}
#degenvault-floaty .dv-header:active { cursor: grabbing; }
#degenvault-floaty .dv-title {
display: flex;
align-items: center;
gap: 7px;
font-weight: 800;
font-size: 12px;
color: var(--hb-accent);
letter-spacing: 1.5px;
text-transform: uppercase;
text-shadow: 0 0 8px rgba(0,255,65,0.5);
}
#degenvault-floaty .dv-title .dv-hb-badge {
background: var(--hb-accent);
color: #000;
font-weight: 900;
font-size: 10px;
padding: 1px 5px;
letter-spacing: 0.5px;
clip-path: polygon(0 4px, 4px 0, 100% 0, 100% 100%, calc(100% - 4px) 100%, 0 100%);
}
#degenvault-floaty .dv-status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #4a5568;
}
#degenvault-floaty .dv-status-dot.running {
background: var(--hb-accent);
box-shadow: 0 0 6px var(--hb-accent);
}
#degenvault-floaty .dv-header-btns { display: flex; gap: 2px; }
#degenvault-floaty .dv-header-btn {
background: none;
border: none;
color: var(--hb-text-dim);
cursor: pointer;
padding: 4px;
border-radius: 4px;
font-size: 14px;
line-height: 1;
}
#degenvault-floaty .dv-header-btn:hover { color: #fff; background: var(--hb-border); }
#degenvault-floaty .dv-content { padding: 12px; display: flex; flex-direction: column; gap: 10px; }
#degenvault-floaty .dv-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
#degenvault-floaty .dv-label { color: var(--hb-text-dim); font-size: 12px; }
#degenvault-floaty .dv-label-group { display: flex; align-items: center; gap: 5px; }
#degenvault-floaty .dv-inline-group { display: flex; align-items: center; gap: 6px; }
#degenvault-floaty .dv-sync-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid var(--hb-border);
background: transparent;
color: var(--hb-text-dim);
font-size: 11px;
line-height: 1;
cursor: pointer;
padding: 0;
flex-shrink: 0;
}
#degenvault-floaty .dv-sync-btn:hover { color: var(--hb-accent); border-color: var(--hb-accent); }
#degenvault-floaty .dv-sync-btn.spinning { animation: dv-spin 0.6s linear; }
@keyframes dv-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
#degenvault-floaty input[type="number"], #degenvault-floaty input[type="text"] {
background: var(--hb-panel-2);
color: #eaffe0;
border: 1px solid var(--hb-border);
border-radius: 4px;
padding: 4px 6px;
font-size: 12px;
width: 70px;
text-align: right;
}
#degenvault-floaty input:focus { outline: none; border-color: var(--hb-accent); }
#degenvault-floaty select {
background: var(--hb-panel-2);
color: #eaffe0;
border: 1px solid var(--hb-border);
border-radius: 4px;
padding: 4px 6px;
font-size: 12px;
min-width: 90px;
}
#degenvault-floaty select:focus { outline: none; border-color: var(--hb-accent); }
#degenvault-floaty .dv-tabs { display: flex; gap: 4px; }
#degenvault-floaty .dv-tab {
flex: 1;
background: transparent;
border: 1px solid var(--hb-border);
border-radius: 3px;
color: var(--hb-text-dim);
padding: 5px 8px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: pointer;
}
#degenvault-floaty .dv-tab:hover { color: var(--hb-text); }
#degenvault-floaty .dv-tab.active {
background: var(--hb-panel-2);
color: var(--hb-accent);
border-color: var(--hb-accent);
}
#degenvault-floaty .dv-pane { display: flex; flex-direction: column; gap: 10px; }
#degenvault-floaty .dv-pane.hidden { display: none; }
#degenvault-floaty .dv-max-btn { flex: 0 0 auto; padding: 4px 8px; font-size: 10px; }
#degenvault-floaty .dv-btn-row { display: flex; gap: 6px; margin-top: 4px; }
#degenvault-floaty .dv-btn {
flex: 1;
background: var(--hb-panel-2);
color: var(--hb-text);
border: 1px solid var(--hb-border);
border-radius: 3px;
padding: 6px 10px;
font-size: 11px;
font-weight: 700;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 0.5px;
}
#degenvault-floaty .dv-btn:hover:not(:disabled) { background: var(--hb-border); color: #fff; }
#degenvault-floaty .dv-btn:disabled { opacity: 0.4; cursor: not-allowed; }
#degenvault-floaty .dv-btn.primary { background: var(--hb-accent); border-color: var(--hb-accent); color: #000; text-shadow: none; }
#degenvault-floaty .dv-btn.primary:hover:not(:disabled) { background: #5cff85; }
#degenvault-floaty .dv-btn.danger { background: #ef4444; border-color: #ef4444; color: #fff; }
#degenvault-floaty .dv-btn.danger:hover:not(:disabled) { background: #dc2626; }
#degenvault-floaty .dv-btn.ghost { background: transparent; }
#degenvault-floaty .dv-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 12px;
padding-top: 8px;
border-top: 1px solid var(--hb-border);
font-size: 11px;
}
#degenvault-floaty .dv-stat { display: flex; flex-direction: column; gap: 2px; }
#degenvault-floaty .dv-stat-label { color: var(--hb-text-dim); font-size: 10px; text-transform: uppercase; display: flex; align-items: center; gap: 4px; }
#degenvault-floaty .dv-stat-value { color: var(--hb-accent); font-weight: 700; text-shadow: 0 0 6px rgba(0,255,65,0.4); }
#degenvault-floaty .dv-footer {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 7px;
border-top: 1px solid var(--hb-border);
}
#degenvault-floaty .dv-footer-tagline span { color: var(--hb-text-dim); font-size: 9.5px; letter-spacing: 1px; text-transform: uppercase; }
#degenvault-floaty .dv-footer .dv-footer-accent { color: var(--hb-accent); }
#degenvault-floaty .dv-discord-link {
display: flex;
align-items: center;
gap: 5px;
color: var(--hb-text-dim);
background: var(--hb-panel-2);
border: 1px solid var(--hb-border);
border-radius: 3px;
padding: 4px 10px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
text-decoration: none;
transition: color 0.15s, border-color 0.15s;
}
#degenvault-floaty .dv-discord-link:hover {
color: var(--hb-accent);
border-color: var(--hb-accent);
text-shadow: 0 0 6px rgba(0,255,65,0.5);
}
#degenvault-floaty .dv-discord-link svg { width: 14px; height: 14px; fill: currentColor; }
#degenvault-floaty .dv-help {
display: inline-flex;
align-items: center;
justify-content: center;
width: 13px;
height: 13px;
border-radius: 50%;
border: 1px solid var(--hb-border);
color: var(--hb-text-dim);
font-size: 9px;
font-weight: 700;
font-style: normal;
text-transform: none;
letter-spacing: 0;
cursor: help;
flex-shrink: 0;
}
#degenvault-floaty .dv-help:hover {
color: #000;
background: var(--hb-accent);
border-color: var(--hb-accent);
}
#degenvault-floaty .dv-title .dv-help { margin-left: -2px; }
#degenvault-tooltip {
position: fixed;
max-width: 210px;
background: #0b100c;
color: #cdf5cf;
border: 1px solid #163019;
border-radius: 4px;
padding: 7px 9px;
font-size: 10px;
font-weight: 400;
line-height: 1.45;
text-align: left;
font-family: 'Segoe UI', Arial, sans-serif;
box-shadow: 0 4px 16px rgba(0,0,0,0.6);
z-index: 2147483647;
opacity: 0;
pointer-events: none;
transition: opacity 0.12s;
}
#degenvault-tooltip.visible { opacity: 1; }
#degenvault-floaty .dv-log-toggle {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
background: var(--hb-panel-2);
border-top: 1px solid var(--hb-border);
cursor: pointer;
}
#degenvault-floaty .dv-log-toggle-text { font-size: 10px; color: var(--hb-text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
#degenvault-floaty .dv-log-toggle-icon { font-size: 10px; color: var(--hb-text-dim); transition: transform 0.2s; }
#degenvault-floaty .dv-log-toggle.open .dv-log-toggle-icon { transform: rotate(180deg); }
#degenvault-floaty .dv-log { max-height: 0; overflow: hidden; transition: max-height 0.25s ease-out; background: #070b06; }
#degenvault-floaty .dv-log.open { max-height: 130px; }
#degenvault-floaty .dv-log-inner { padding: 8px; max-height: 130px; overflow-y: auto; font-family: Monaco, Consolas, monospace; font-size: 10px; line-height: 1.4; }
#degenvault-floaty .dv-log-entry { padding: 2px 0; color: var(--hb-text-dim); display: flex; gap: 6px; }
#degenvault-floaty .dv-log-entry.success { color: var(--hb-accent); }
#degenvault-floaty .dv-log-entry.profit { color: var(--hb-accent); }
#degenvault-floaty .dv-log-entry.bigwin { color: #fbbf24; }
#degenvault-floaty .dv-log-entry.warning { color: #f59e0b; }
#degenvault-floaty .dv-log-entry.error { color: #ef4444; }
#degenvault-floaty .dv-log-time { color: #3d4d38; flex-shrink: 0; }
#degenvault-floaty .dv-log-empty { color: #3d4d38; font-style: italic; text-align: center; padding: 8px; }
#degenvault-floaty.mini { min-width: auto; max-width: none; border-radius: 20px; }
#degenvault-floaty.mini .dv-header { border-radius: 20px; padding: 6px 12px; border-bottom: none; }
#degenvault-floaty.mini .dv-content, #degenvault-floaty.mini .dv-log-toggle,
#degenvault-floaty.mini .dv-log, #degenvault-floaty.mini .dv-footer { display: none; }
#degenvault-floaty.mini .dv-title span:not(.dv-hb-badge) { display: none; }
#degenvault-stealth {
position: fixed;
bottom: 10px;
right: 10px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #4a5568;
cursor: pointer;
z-index: 999999;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
#degenvault-stealth:hover { transform: scale(1.5); }
#degenvault-stealth.running { background: #00FF41; }
#degenvault-stealth.hidden { display: none; }
@media (max-width: 500px) {
#degenvault-floaty { max-width: none; min-width: auto; }
#degenvault-floaty:not(.dv-dragged) { right: 10px !important; left: 10px !important; }
}
`;
document.head.appendChild(style);
const widget = document.createElement('div');
widget.id = 'degenvault-floaty';
const stealthDot = document.createElement('div');
stealthDot.id = 'degenvault-stealth';
stealthDot.className = 'hidden';
stealthDot.title = 'DegenVault (click to expand)';
document.body.appendChild(stealthDot);
const header = document.createElement('div');
header.className = 'dv-header';
header.innerHTML = `
`;
widget.appendChild(header);
const content = document.createElement('div');
content.className = 'dv-content';
content.innerHTML = `
Currency
?
Amount
?
Available
β
Wallet Balance
β
Vault Balance
0.00
Actions/hr
0/50
7d Wager ?
β
`;
widget.appendChild(content);
const logToggle = document.createElement('div');
logToggle.className = 'dv-log-toggle';
logToggle.innerHTML = `Activity LogβΌ`;
widget.appendChild(logToggle);
const logPanel = document.createElement('div');
logPanel.className = 'dv-log';
logPanel.innerHTML = ``;
widget.appendChild(logPanel);
const logInner = logPanel.querySelector('#dvLogInner');
logToggle.onclick = () => {
logToggle.classList.toggle('open');
logPanel.classList.toggle('open');
};
const formatTime = (date) => {
const h = date.getHours().toString().padStart(2, '0');
const m = date.getMinutes().toString().padStart(2, '0');
const s = date.getSeconds().toString().padStart(2, '0');
return `${h}:${m}:${s}`;
};
function addLogEntry(entry) {
const empty = logInner.querySelector('.dv-log-empty');
if (empty) empty.remove();
const div = document.createElement('div');
div.className = `dv-log-entry ${entry.type}`;
div.innerHTML = `${formatTime(entry.time)}${entry.message}`;
logInner.insertBefore(div, logInner.firstChild);
while (logInner.children.length > 20) logInner.removeChild(logInner.lastChild);
}
onLogUpdate = addLogEntry;
const footer = document.createElement('div');
footer.className = 'dv-footer';
footer.innerHTML = `
Discord
`;
widget.appendChild(footer);
const vaultBalEl = content.querySelector('#dvVaultBal');
vaultDisplay._el = vaultBalEl;
vaultDisplay.render();
const statusDot = widget.querySelector('#dvStatusDot');
const minBtn = widget.querySelector('#dvMinBtn');
const stealthBtn = widget.querySelector('#dvStealthBtn');
const closeBtn = widget.querySelector('#dvCloseBtn');
function setViewMode(mode) {
currentViewMode = mode;
if (mode === 'full') {
widget.classList.remove('mini', 'hidden');
stealthDot.classList.add('hidden');
} else if (mode === 'mini') {
widget.classList.add('mini');
widget.classList.remove('hidden');
stealthDot.classList.add('hidden');
} else if (mode === 'stealth') {
widget.classList.add('hidden');
stealthDot.classList.remove('hidden');
}
}
minBtn.onclick = (e) => {
e.stopPropagation();
setViewMode(currentViewMode === 'mini' ? 'full' : 'mini');
minBtn.textContent = currentViewMode === 'mini' ? '+' : 'β';
};
stealthBtn.onclick = (e) => { e.stopPropagation(); setViewMode('stealth'); };
stealthDot.onclick = () => { setViewMode('full'); minBtn.textContent = 'β'; };
closeBtn.onclick = () => { widget.remove(); stealthDot.remove(); };
// Drag: mouse + touch, with the position persisted so the widget stays
// where the user put it across page loads. On phones the widget is
// stretched edge-to-edge by CSS until the first drag; the width is
// locked at that moment so it doesn't snap while moving.
const POS_KEY = 'degenvault-widget-pos';
let isDragging = false, dragOffsetX = 0, dragOffsetY = 0;
function savePosition() {
try {
localStorage.setItem(POS_KEY, JSON.stringify({
left: parseFloat(widget.style.left),
top: parseFloat(widget.style.top),
width: parseFloat(widget.style.width) || null
}));
} catch (e) {}
}
function restorePosition() {
let pos = null;
try { pos = JSON.parse(localStorage.getItem(POS_KEY)); } catch (e) {}
if (!pos || !isFinite(pos.left) || !isFinite(pos.top)) return;
widget.classList.add('dv-dragged');
if (isFinite(pos.width) && pos.width > 0) widget.style.width = Math.min(pos.width, window.innerWidth - 8) + 'px';
widget.style.left = Math.max(0, Math.min(window.innerWidth - widget.offsetWidth, pos.left)) + 'px';
widget.style.top = Math.max(0, Math.min(window.innerHeight - 40, pos.top)) + 'px';
widget.style.right = 'auto';
}
function dragStart(x, y, target) {
if (target.closest && target.closest('.dv-header-btns')) return false;
const rect = widget.getBoundingClientRect();
isDragging = true;
dragOffsetX = x - rect.left;
dragOffsetY = y - rect.top;
return true;
}
function dragMove(x, y) {
if (!isDragging) return;
if (!widget.classList.contains('dv-dragged')) {
widget.style.width = widget.getBoundingClientRect().width + 'px';
widget.classList.add('dv-dragged');
}
let newLeft = x - dragOffsetX;
let newTop = y - dragOffsetY;
newLeft = Math.max(0, Math.min(window.innerWidth - widget.offsetWidth, newLeft));
newTop = Math.max(0, Math.min(window.innerHeight - widget.offsetHeight, newTop));
widget.style.left = newLeft + 'px';
widget.style.top = newTop + 'px';
widget.style.right = 'auto';
}
function dragEnd() {
if (!isDragging) return;
isDragging = false;
if (widget.classList.contains('dv-dragged') && widget.style.left) savePosition();
}
header.addEventListener('mousedown', function(e) {
if (dragStart(e.clientX, e.clientY, e.target)) e.preventDefault();
});
document.addEventListener('mousemove', (e) => dragMove(e.clientX, e.clientY));
document.addEventListener('mouseup', dragEnd);
header.addEventListener('touchstart', function(e) {
const t = e.touches[0];
if (t && dragStart(t.clientX, t.clientY, e.target)) e.preventDefault();
}, { passive: false });
document.addEventListener('touchmove', function(e) {
if (!isDragging) return;
const t = e.touches[0];
if (t) { dragMove(t.clientX, t.clientY); e.preventDefault(); }
}, { passive: false });
document.addEventListener('touchend', dragEnd);
document.addEventListener('touchcancel', dragEnd);
const startBtn = content.querySelector('#vaultStartBtn');
const saveBtn = content.querySelector('#vaultSaveBtn');
const stopBtn = content.querySelector('#vaultStopBtn');
const saveAmountInput = content.querySelector('#vaultSaveAmount');
const bigWinThresholdInput = content.querySelector('#vaultBigWinThreshold');
const bigWinMultiplierInput = content.querySelector('#vaultBigWinMultiplier');
const checkIntervalInput = content.querySelector('#vaultCheckInterval');
// Read every setting straight from the inputs, clamp, and write the
// clamped values back. Called on Start and Save so the numbers shown
// are always exactly what gets used β mobile keyboards don't reliably
// fire the change event before a button tap, so relying on onchange
// alone silently reverted edits.
function readSettingsFromInputs() {
let sa = parseFloat(saveAmountInput.value);
if (isNaN(sa) || sa < 0) sa = 0;
if (sa > 1) sa = 1;
let bt = parseFloat(bigWinThresholdInput.value);
if (isNaN(bt) || bt < 1) bt = 1;
let bm = parseFloat(bigWinMultiplierInput.value);
if (isNaN(bm) || bm < 1) bm = 1;
let ci = parseInt(checkIntervalInput.value, 10);
if (isNaN(ci) || ci < 10) ci = 10;
saveAmountInput.value = sa;
bigWinThresholdInput.value = bt;
bigWinMultiplierInput.value = bm;
checkIntervalInput.value = ci;
return { saveAmount: sa, bigWinThreshold: bt, bigWinMultiplier: bm, checkInterval: ci };
}
const vaultCountEl = content.querySelector('#dvVaultCount');
const walletBalEl = content.querySelector('#dvWalletBal');
const trackedAssetEl = content.querySelector('#dvTrackedAsset');
const syncBtn = content.querySelector('#vaultSyncBtn');
function updateVaultCountUI() {
const count = getVaultCountLastHour();
vaultCountEl.textContent = `${count}/50`;
vaultCountEl.style.color = count >= 50 ? '#ef4444' : count >= 40 ? '#f59e0b' : '#00FF41';
}
window.__updateVaultCountUI = updateVaultCountUI;
updateVaultCountUI();
setInterval(updateVaultCountUI, 10000);
function setRunningState(isRunning) {
statusDot.classList.toggle('running', isRunning);
stealthDot.classList.toggle('running', isRunning);
startBtn.disabled = isRunning;
stopBtn.disabled = !isRunning;
}
startBtn.onclick = () => {
setParams(readSettingsFromInputs(), { restart: false });
setRunningState(true);
startCallback();
updateVaultCountUI();
};
saveBtn.onclick = () => {
setParams(readSettingsFromInputs());
logActivity('Settings saved', 'success');
saveBtn.textContent = 'Saved β';
setTimeout(() => { saveBtn.textContent = 'Save'; }, 1200);
};
stopBtn.onclick = () => { setRunningState(false); stopCallback(); updateVaultCountUI(); };
function updateWalletBalanceUI(value) {
if (walletBalEl) walletBalEl.textContent = (typeof value === 'number' && !isNaN(value)) ? value.toFixed(8) : 'β';
}
window.__updateWalletBalanceUI = updateWalletBalanceUI;
function updateTrackedAssetUI(asset) {
if (trackedAssetEl) trackedAssetEl.textContent = asset || 'Waitingβ¦';
}
if (syncBtn) {
syncBtn.onclick = () => {
syncBtn.classList.add('spinning');
syncBtn.disabled = true;
Promise.resolve(syncCallback()).finally(() => {
syncBtn.classList.remove('spinning');
syncBtn.disabled = false;
});
};
}
// --- Tabs (Auto / Manual) ---
const tabAuto = content.querySelector('#dvTabAuto');
const tabManual = content.querySelector('#dvTabManual');
const paneAuto = content.querySelector('#dvPaneAuto');
const paneManual = content.querySelector('#dvPaneManual');
function showTab(which) {
const manual = which === 'manual';
tabAuto.classList.toggle('active', !manual);
tabManual.classList.toggle('active', manual);
paneAuto.classList.toggle('hidden', manual);
paneManual.classList.toggle('hidden', !manual);
if (manual) populateManualAssets();
}
tabAuto.onclick = () => showTab('auto');
tabManual.onclick = () => showTab('manual');
// --- Manual vaulting ---
const manualAssetSelect = content.querySelector('#dvManualAsset');
const manualRefreshBtn = content.querySelector('#dvManualRefresh');
const manualAmountInput = content.querySelector('#dvManualAmount');
const manualMaxBtn = content.querySelector('#dvManualMax');
const manualAvailEl = content.querySelector('#dvManualAvail');
const manualDepositBtn = content.querySelector('#dvManualDeposit');
let manualWallets = [];
function manualSelected() {
return manualWallets.find(w => (w.asset || '').toUpperCase() === manualAssetSelect.value) || null;
}
function updateManualAvail() {
const w = manualSelected();
const bal = w ? parseFloat(w.balance) : NaN;
manualAvailEl.textContent = !isNaN(bal) ? `${bal.toFixed(8)} ${(w.asset || '').toUpperCase()}` : 'β';
}
async function populateManualAssets() {
manualRefreshBtn.classList.add('spinning');
const prev = manualAssetSelect.value;
const list = await fetchWalletsCallback();
manualRefreshBtn.classList.remove('spinning');
if (!Array.isArray(list)) {
if (!manualWallets.length) manualAvailEl.textContent = 'API error';
return;
}
manualWallets = list;
manualAssetSelect.innerHTML = '';
for (const w of list) {
const a = (w.asset || '').toUpperCase();
if (!a) continue;
const opt = document.createElement('option');
opt.value = a;
opt.textContent = a;
manualAssetSelect.appendChild(opt);
}
const want = prev || (ASSET || '').toUpperCase();
if (want && [...manualAssetSelect.options].some(o => o.value === want)) manualAssetSelect.value = want;
updateManualAvail();
}
manualAssetSelect.onchange = updateManualAvail;
manualRefreshBtn.onclick = () => populateManualAssets();
manualMaxBtn.onclick = () => {
const w = manualSelected();
const bal = w ? parseFloat(w.balance) : NaN;
if (!isNaN(bal)) manualAmountInput.value = bal.toFixed(8);
};
manualDepositBtn.onclick = async () => {
const asset = manualAssetSelect.value;
const amt = parseFloat(manualAmountInput.value);
if (!asset) { logActivity('Manual vault: pick a currency first', 'warning'); return; }
if (isNaN(amt) || amt <= 0) { logActivity('Manual vault: enter an amount above 0', 'warning'); return; }
const w = manualSelected();
const bal = w ? parseFloat(w.balance) : NaN;
if (!isNaN(bal) && amt > bal + 1e-9) {
logActivity(`Manual vault: amount exceeds available ${bal.toFixed(8)} ${asset}`, 'warning');
return;
}
manualDepositBtn.disabled = true;
manualDepositBtn.textContent = 'Vaultingβ¦';
const ok = await manualVaultCallback(asset, amt);
manualDepositBtn.textContent = ok ? 'Vaulted β' : 'Failed β';
setTimeout(() => {
manualDepositBtn.textContent = 'Deposit to Vault';
manualDepositBtn.disabled = false;
}, 1400);
if (ok) {
manualAmountInput.value = '';
populateManualAssets();
}
};
saveAmountInput.onchange = function() {
let v = parseFloat(this.value);
if (isNaN(v) || v < 0) v = 0;
if (v > 1) v = 1;
setParams({saveAmount: v});
this.value = v;
};
bigWinThresholdInput.onchange = function() {
let v = parseFloat(this.value);
if (isNaN(v) || v < 1) v = 1;
setParams({bigWinThreshold: v});
this.value = v;
};
bigWinMultiplierInput.onchange = function() {
let v = parseFloat(this.value);
if (isNaN(v) || v < 1) v = 1;
setParams({bigWinMultiplier: v});
this.value = v;
};
checkIntervalInput.onchange = function() {
let v = parseInt(this.value, 10);
if (isNaN(v) || v < 10) v = 10;
setParams({checkInterval: v});
this.value = v;
};
// Persist as the user types (no clamping mid-edit, no restart) so the
// value survives an SPA reload even if change never fires on mobile.
saveAmountInput.addEventListener('input', function() {
const v = parseFloat(this.value);
if (!isNaN(v)) setParams({saveAmount: Math.max(0, Math.min(1, v))}, { restart: false });
});
bigWinThresholdInput.addEventListener('input', function() {
const v = parseFloat(this.value);
if (!isNaN(v)) setParams({bigWinThreshold: Math.max(1, v)}, { restart: false });
});
bigWinMultiplierInput.addEventListener('input', function() {
const v = parseFloat(this.value);
if (!isNaN(v)) setParams({bigWinMultiplier: Math.max(1, v)}, { restart: false });
});
checkIntervalInput.addEventListener('input', function() {
const v = parseInt(this.value, 10);
if (!isNaN(v)) setParams({checkInterval: Math.max(10, v)}, { restart: false });
});
document.body.appendChild(widget);
restorePosition();
initHelpTooltips(widget);
return {
setRunning: setRunningState,
updateVaultCount: updateVaultCountUI,
updateWalletBalance: updateWalletBalanceUI,
updateTrackedAsset: updateTrackedAssetUI
};
}
// --- Main logic ---
let vaultInterval = null;
let vaultDisplay = null;
let degenApi = null;
let apiVaultPollInterval = null;
let oldBalance = 0;
let isProcessing = false;
let isInitialized = false;
let lastBalance = 0;
let running = false;
let uiWidget = null;
async function refreshApiVaultBalance() {
try {
if (!ASSET || !vaultDisplay) return;
if (!degenApi) degenApi = new DegenApi();
const resp = await degenApi.getVaults();
if (!Array.isArray(resp)) return;
const entry = resp.find(x => (x.asset || '').toUpperCase() === ASSET.toUpperCase());
if (entry) vaultDisplay.setApiVaultBalance(entry.balance);
} catch (e) {}
}
function startApiVaultPolling() {
if (apiVaultPollInterval) clearInterval(apiVaultPollInterval);
apiVaultPollInterval = setInterval(refreshApiVaultBalance, 15000);
refreshApiVaultBalance();
}
function stopApiVaultPolling() {
if (apiVaultPollInterval) clearInterval(apiVaultPollInterval);
apiVaultPollInterval = null;
}
// ββ Rolling 7-day wager tracker βββββββββββββββββββββββββββββββββββββ
// Same design as the Stake claimer's tracker: read the server-side lifetime
// wagered counter (Degen's user statistics, USD-equivalent across all
// currencies and devices), snapshot it per-account in localStorage, and
// difference over 168h to get the rolling-7d wager. Because the lifetime
// counter is authoritative, gaps in snapshotting (tab closed, phone off)
// never lose wager β the next reading self-corrects; only granularity of
// where the 7-day boundary falls is affected.
const WAGER_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
const WAGER_KEEP_MS = 9 * 24 * 60 * 60 * 1000;
const WAGER_POLL_MS = 60 * 1000;
let wagerUserId = null;
let wagerLifetime = null;
let wagerRolling = null;
let wagerOk = false;
let wagerPollTimer = null;
let wagerBackoffUntil = 0;
// Keys are namespaced per user id so switching accounts in one browser
// never interleaves two different lifetime scales in one anchor array.
// Null until the account is known β better not to snapshot at all than
// to snapshot into the wrong bucket.
function wagerKeys() {
if (!wagerUserId) return null;
return {
anchors: `degenvault-wager-anchors::${wagerUserId}`,
start: `degenvault-wager-start::${wagerUserId}`
};
}
function wagerGet(key, fallback) {
try {
const raw = localStorage.getItem(key);
return raw == null ? fallback : JSON.parse(raw);
} catch (e) { return fallback; }
}
function wagerSet(key, value) {
try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) {}
}
// Compact figure for the stat box (120000 -> "$120.0k").
function wagerFmtShort(n) {
if (n == null || !isFinite(n)) return 'β';
if (n >= 1e6) return '$' + (n / 1e6).toFixed(2) + 'M';
if (n >= 1e3) return '$' + (n / 1e3).toFixed(1) + 'k';
return '$' + Math.round(n);
}
// Degen reports lifetime wagered as a USD figure β sometimes a bare number,
// sometimes a string like "12,345.67 USD".
function wagerParseAmount(w) {
if (typeof w === 'number') return w;
if (typeof w !== 'string') return NaN;
return parseFloat(w.replace(/[^0-9.]/g, ''));
}
// Append a lifetime snapshot (monotonic; throttle unchanged; prune past
// window+margin, but always keep the newest anchor already β₯7d old β that
// one is the rolling baseline).
function wagerRecordAnchor(cum) {
if (!isFinite(cum) || cum < 0) return;
const K = wagerKeys();
if (!K) return;
const now = Date.now();
let anchors = wagerGet(K.anchors, []);
if (!Array.isArray(anchors)) anchors = [];
const last = anchors[anchors.length - 1];
if (last) {
if (cum < last.cum - 1e-6) return; // lifetime can't drop
if (Math.abs(cum - last.cum) < 1e-9 && (now - last.t) < 10 * 60 * 1000) return; // throttle unchanged
}
anchors.push({ t: now, cum });
const minT = now - WAGER_KEEP_MS, cutoff = now - WAGER_WINDOW_MS;
let keepIdx = -1;
for (let i = 0; i < anchors.length; i++) if (anchors[i].t <= cutoff) keepIdx = i;
anchors = anchors.filter((p, idx) => p.t >= minT || idx === keepIdx);
wagerSet(K.anchors, anchors);
}
// rolling 168h = lifetime(now) β lifetime(NEWEST snapshot already β₯7d old).
// With no anchor that old yet (warm-up) the oldest anchor is the best lower
// bound; the UI shows amber until the full window is covered.
function wagerComputeRolling() {
const K = wagerKeys();
if (!K) return null;
const anchors = wagerGet(K.anchors, []);
if (!Array.isArray(anchors) || !anchors.length) return null;
const now = Date.now(), cutoff = now - WAGER_WINDOW_MS;
const cur = anchors[anchors.length - 1].cum;
let baseline = null;
for (let i = 0; i < anchors.length; i++) { if (anchors[i].t <= cutoff) baseline = anchors[i]; }
if (!baseline) baseline = anchors[0];
return Math.max(0, cur - baseline.cum);
}
function wagerCoverageMs() {
const K = wagerKeys();
const start = K ? wagerGet(K.start, 0) : 0;
return start ? (Date.now() - start) : 0;
}
async function fetchWagerStats() {
// Back off after a failure (usually logged out) so the activity log
// isn't flooded with a failed API call every poll.
if (Date.now() < wagerBackoffUntil) return;
try {
if (!degenApi) degenApi = new DegenApi();
if (!wagerUserId) {
const prof = await degenApi.getProfile();
const id = prof && !prof.error ? (prof.id || (prof.user && prof.user.id)) : null;
if (!id) { wagerFail(); return; }
wagerUserId = String(id);
}
// The public user object is where Degen's own frontend reads
// statistics from; /users/{id}/stats is the fallback.
let stats = null;
const pub = await degenApi.getPublicUser(wagerUserId);
if (pub && !pub.error) stats = pub.statistics || pub.stats || null;
if (!stats) {
const alt = await degenApi.getUserStats(wagerUserId);
if (alt && !alt.error) stats = alt.statistics || alt;
}
const lifetime = stats ? wagerParseAmount(stats.wagered) : NaN;
if (isNaN(lifetime)) { wagerFail(); return; }
const first = wagerLifetime == null;
wagerOk = true;
wagerBackoffUntil = 0;
wagerLifetime = lifetime;
const K = wagerKeys();
if (K && !wagerGet(K.start, 0)) wagerSet(K.start, Date.now()); // start the coverage clock
wagerRecordAnchor(lifetime);
wagerRolling = wagerComputeRolling();
if (first) logActivity(`7d wager tracking online β lifetime wagered $${Math.round(lifetime).toLocaleString()}`, 'info');
updateWagerUI();
} catch (e) {
wagerFail();
}
}
function wagerFail() {
wagerOk = false;
wagerBackoffUntil = Date.now() + 5 * 60 * 1000;
updateWagerUI();
}
// Runs whenever a Degen tab is open, independent of Start/Stop β the
// rolling window is only as good as its snapshot history.
function startWagerTracking() {
if (wagerPollTimer) return;
fetchWagerStats();
wagerPollTimer = setInterval(fetchWagerStats, WAGER_POLL_MS);
document.addEventListener('visibilitychange', () => { if (!document.hidden) fetchWagerStats(); });
}
function updateWagerUI() {
const el = document.getElementById('dvWager7d');
if (!el) return;
if (wagerRolling == null) {
el.textContent = 'β';
el.style.color = '';
el.title = wagerOk
? 'First reading in β computing the windowβ¦'
: 'Waiting for the first wager reading (log in to Degen if this persists)';
return;
}
const coverMs = wagerCoverageMs();
const full = coverMs >= WAGER_WINDOW_MS;
el.textContent = wagerFmtShort(wagerRolling);
el.style.color = full ? '' : '#f59e0b'; // amber while the window warms up
el.title =
`Rolling 7d: $${Math.round(wagerRolling).toLocaleString()}\n` +
`Lifetime: ${wagerLifetime != null ? '$' + Math.round(wagerLifetime).toLocaleString() : 'β'}\n` +
(full ? 'Coverage: full 7d window β
'
: `Coverage: warming up ${(coverMs / 864e5).toFixed(1)}/7d β reads LOW until a full week is covered`);
}
// F12 console helpers: __degenWagerStatus() / __degenWagerRefresh()
window.__degenWagerRefresh = () => fetchWagerStats();
window.__degenWagerStatus = () => {
const K = wagerKeys();
const anchors = K ? wagerGet(K.anchors, []) : [];
const coverD = wagerCoverageMs() / 864e5;
return {
userId: wagerUserId, rolling7d: wagerRolling, lifetime: wagerLifetime,
anchors: Array.isArray(anchors) ? anchors.length : 0,
coverageDays: +coverD.toFixed(2), full: coverD >= 7, ok: wagerOk
};
};
function getParams() {
return {
saveAmount: SAVE_AMOUNT,
bigWinThreshold: BIG_WIN_THRESHOLD,
bigWinMultiplier: BIG_WIN_MULTIPLIER,
checkInterval: Math.round(CHECK_INTERVAL/1000)
};
}
function setParams(obj, opts) {
if (obj.saveAmount !== undefined) SAVE_AMOUNT = obj.saveAmount;
if (obj.bigWinThreshold !== undefined) BIG_WIN_THRESHOLD = obj.bigWinThreshold;
if (obj.bigWinMultiplier !== undefined) BIG_WIN_MULTIPLIER = obj.bigWinMultiplier;
if (obj.checkInterval !== undefined) CHECK_INTERVAL = obj.checkInterval * 1000;
config = {
saveAmount: SAVE_AMOUNT,
bigWinThreshold: BIG_WIN_THRESHOLD,
bigWinMultiplier: BIG_WIN_MULTIPLIER,
checkInterval: CHECK_INTERVAL
};
saveConfig(config);
const restart = !opts || opts.restart !== false;
if (restart && running) {
stopVaultScript();
startVaultScript();
}
}
async function fetchWalletBalance() {
if (!degenApi) degenApi = new DegenApi();
const resp = await degenApi.getWallets();
if (!Array.isArray(resp)) {
if (resp && resp.error) log(`Balance check failed: ${resp.message || resp.status}`);
return NaN;
}
const primary = resp.find(x => x.isPrimary);
if (!primary) {
log('β οΈ No primary wallet found.');
return NaN;
}
setTrackedAsset(primary.asset);
const n = parseFloat(primary.balance);
return isNaN(n) ? NaN : n;
}
async function fetchWalletsList() {
if (!degenApi) degenApi = new DegenApi();
const resp = await degenApi.getWallets();
return Array.isArray(resp) ? resp : null;
}
// Manual one-off vault deposit from the Manual tab. Counts against the same
// client-side rate limit as auto deposits. Returns true on success.
let manualProcessing = false;
async function manualVault(asset, amount) {
if (manualProcessing) return false;
if (!canVaultNow()) {
logActivity(pickFlavor(FLAVOR.rateLimit), 'warning');
if (uiWidget) uiWidget.updateVaultCount();
return false;
}
manualProcessing = true;
try {
if (!degenApi) degenApi = new DegenApi();
const resp = await degenApi.depositToVault(asset, amount);
if (resp && !resp.error && resp.vaultBalance !== undefined) {
vaultActionTimestamps.push(Date.now());
saveRateLimitData(vaultActionTimestamps);
if (vaultDisplay && (asset || '').toUpperCase() === (ASSET || '').toUpperCase()) {
vaultDisplay.update(amount);
vaultDisplay.setApiVaultBalance(resp.vaultBalance);
if (resp.walletBalance !== undefined) {
const wb = parseFloat(resp.walletBalance);
if (!isNaN(wb)) {
oldBalance = wb;
if (uiWidget) uiWidget.updateWalletBalance(wb);
}
}
}
if (uiWidget) uiWidget.updateVaultCount();
logActivity(`Secured ${(+amount).toFixed(6)} ${asset} (manual)`, 'success');
return true;
}
logActivity('Manual vault failed β check amount, balance and rate limit', 'error');
return false;
} catch (e) {
logActivity('Manual vault error: ' + (e.message || 'unknown'), 'error');
return false;
} finally {
manualProcessing = false;
}
}
async function syncCurrency() {
logActivity('Syncing currencyβ¦', 'info');
const cur = await fetchWalletBalance();
if (uiWidget) uiWidget.updateWalletBalance(cur);
if (!isNaN(cur)) {
logActivity(`Synced β currently tracking ${ASSET}`, 'success');
} else {
logActivity('Sync failed β could not reach the balance API.', 'error');
}
}
async function processDeposit(amount, isBigWin) {
if (amount < 1e-8 || isProcessing) return;
if (!canVaultNow()) {
logActivity(`${pickFlavor(FLAVOR.rateLimit)}`, 'warning');
if (uiWidget) uiWidget.updateVaultCount();
return;
}
isProcessing = true;
const pct = (SAVE_AMOUNT * (isBigWin ? BIG_WIN_MULTIPLIER : 1) * 100).toFixed(0);
const flavor = pickFlavor(isBigWin ? FLAVOR.bigWin : FLAVOR.profit);
logActivity(`${flavor} β vaulting ${pct}%: ${amount.toFixed(6)} ${ASSET}`, isBigWin ? 'bigwin' : 'profit');
try {
const resp = await degenApi.depositToVault(ASSET, amount);
isProcessing = false;
if (resp && !resp.error && resp.vaultBalance !== undefined) {
vaultDisplay.update(amount);
vaultDisplay.setApiVaultBalance(resp.vaultBalance);
vaultActionTimestamps.push(Date.now());
saveRateLimitData(vaultActionTimestamps);
if (resp.walletBalance !== undefined) {
const wb = parseFloat(resp.walletBalance);
if (!isNaN(wb)) {
oldBalance = wb;
if (uiWidget) uiWidget.updateWalletBalance(wb);
}
}
if (uiWidget) uiWidget.updateVaultCount();
logActivity(`Secured ${amount.toFixed(6)} ${ASSET}`, 'success');
} else {
logActivity('Vault failed β may be rate limited or asset mismatch', 'error');
}
} catch (e) {
isProcessing = false;
logActivity('Vault error: ' + (e.message || 'unknown'), 'error');
}
}
async function checkBalanceChanges() {
const cur = await fetchWalletBalance();
if (uiWidget) uiWidget.updateWalletBalance(cur);
if (isNaN(cur)) return;
if (!isInitialized) {
oldBalance = cur;
isInitialized = true;
log(`πΎ Initial balance: ${oldBalance.toFixed(8)} ${ASSET}`);
lastBalance = cur;
return;
}
if (cur > oldBalance) {
const profit = cur - oldBalance;
const isBig = cur > oldBalance * BIG_WIN_THRESHOLD;
const depAmt = profit * SAVE_AMOUNT * (isBig ? BIG_WIN_MULTIPLIER : 1);
processDeposit(depAmt, isBig);
oldBalance = cur;
} else if (cur < oldBalance) {
oldBalance = cur;
}
lastBalance = cur;
if (uiWidget) uiWidget.updateVaultCount();
}
async function startVaultScript() {
if (running) return;
isScriptInitialized = true;
running = true;
logActivity(pickFlavor(FLAVOR.start), 'success');
if (!vaultDisplay) vaultDisplay = new VaultDisplay();
degenApi = new DegenApi();
startApiVaultPolling();
if (ASSET) vaultDisplay.setAsset(ASSET);
oldBalance = 0;
isProcessing = false;
isInitialized = false;
lastBalance = 0;
vaultActionTimestamps = [];
await checkBalanceChanges();
vaultInterval = setInterval(checkBalanceChanges, CHECK_INTERVAL);
if (uiWidget) {
uiWidget.setRunning(true);
uiWidget.updateVaultCount();
}
}
function stopVaultScript() {
if (!running) return;
running = false;
isScriptInitialized = false;
if (vaultInterval) clearInterval(vaultInterval);
vaultInterval = null;
stopApiVaultPolling();
if (uiWidget) {
uiWidget.setRunning(false);
uiWidget.updateVaultCount();
}
logActivity(pickFlavor(FLAVOR.stop), 'info');
}
// --- UI Widget setup (floaty) ---
setTimeout(() => {
if (!uiWidget) {
if (!vaultDisplay) vaultDisplay = new VaultDisplay();
uiWidget = createVaultFloatyUI(
startVaultScript,
stopVaultScript,
getParams,
setParams,
vaultDisplay,
syncCurrency,
fetchWalletsList,
manualVault
);
vaultDisplay.setAsset(ASSET);
refreshApiVaultBalance();
startWagerTracking();
updateWagerUI();
}
}, INIT_DELAY);
})();