Enforce verified-only wallet monitoring
- Restore persistent verification: wallets.js loadWallets() now preserves isVerified, signature, messageData from localStorage (was reset to false). Only restores verification when messageData is present (anti-tampering). - Add WalletManager.getVerifiedWallets() for EVM-only verification gating. - Remove all hardcoded embedded data (walletCumulData, walletsMetadata, walletCosts, walletBuys, dailySatsData, aaveCumulData, totalCumulData). - Derive all chart data dynamically from API snapshots. - Rewrite calculateAggregatedSeries, calculateCurrentHoldings, setupSatsCard, setupBreakdownCard to source from addressSnapshots. - Replace fetchAaveSnapshots with fetchAllWalletData (verified wallets only). - Replace pollAaveUpdate with pollVerifiedWallets polling loop. - Add inline EIP-712 verification (verifyOwnership, handleVerifyWallet, handleRevokeVerification) with MetaMask integration. - Sidebar shows verified/unverified badges per wallet with verify/revoke buttons. - Non-EVM wallets (btc, solana, bitcoin) auto-verify on add (trust-based). - Update fallback WalletManager class identically with persistent verification.
This commit is contained in:
441
index.html
441
index.html
@ -331,7 +331,8 @@ window.WalletManager = WalletManager;
|
||||
const raw = localStorage.getItem('cbbtc_tracked_wallets');
|
||||
this._wallets = raw ? JSON.parse(raw) : [];
|
||||
if (!Array.isArray(this._wallets)) this._wallets = [];
|
||||
this._wallets = this._wallets.filter(w => w && w.address && w.chain).map(w => ({ address: String(w.address), chain: String(w.chain).toLowerCase(), nickname: String(w.nickname||''), isVerified: false, signature: null, messageData: null }));
|
||||
this._wallets = this._wallets.filter(w => w && w.address && w.chain).map(w => ({ address: String(w.address), chain: String(w.chain).toLowerCase(), nickname: String(w.nickname||''), isVerified: !!(w.isVerified && w.messageData), signature: w.signature ? String(w.signature) : null, messageData: w.messageData ? w.messageData : null }));
|
||||
this.getVerifiedWallets = function() { return this._wallets.filter(w => ['btc','bitcoin','solana'].includes(w.chain) ? true : w.isVerified).map(w => ({...w})); };
|
||||
} catch(e) { this._wallets = []; }
|
||||
return this._wallets;
|
||||
}
|
||||
@ -356,13 +357,31 @@ window.WalletManager = WalletManager;
|
||||
return { success: true, removedAddress: address };
|
||||
}
|
||||
findWallet(address, chain) { return this._wallets.find(w => w.address === address && w.chain === chain); }
|
||||
renameWallet(address, chain, nickname) {
|
||||
renameWallet(address, chain, nickname) {
|
||||
const w = this.findWallet(address, chain);
|
||||
if (!w) return;
|
||||
w.nickname = String(nickname).trim();
|
||||
this._persist();
|
||||
}
|
||||
};
|
||||
}
|
||||
verifyWallet(address, chain, signature, messageData) {
|
||||
const w = this.findWallet(address, chain);
|
||||
if (!w) return { success: false, error: 'Wallet not found' };
|
||||
w.isVerified = true;
|
||||
w.signature = String(signature);
|
||||
w.messageData = messageData || null;
|
||||
this._persist();
|
||||
return { success: true };
|
||||
}
|
||||
revokeVerification(address, chain) {
|
||||
const w = this.findWallet(address, chain);
|
||||
if (!w) return { success: false, error: 'Wallet not found' };
|
||||
w.isVerified = false;
|
||||
w.signature = null;
|
||||
w.messageData = null;
|
||||
this._persist();
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
@ -375,16 +394,6 @@ const API_BASE = window.location.origin;
|
||||
const WALLET_COLORS = ['#F7931A','#FF007F','#39FF14','#00FFFF','#CCFF00','#9D00FF','#FF0033','#00FFCC','#FF00FF','#007FFF','#DEFF0A','#FF5E00','#8A2BE2','#00FF66','#FF1493','#7B00FF'];
|
||||
const TOKENS = {"cbBTC": {"decimals": 8, "priceSymbol": "BTC"}, "WETH": {"decimals": 18, "priceSymbol": "WETH"}, "USDC": {"decimals": 6, "priceSymbol": "USDC"}};
|
||||
|
||||
/* Embedded data for hardcoded wallets */
|
||||
const walletCumulData = {"penguin": [[1732057200000, 0.0], [1732143600000, 0.0], [1732230000000, 0.0], [1732316400000, 0.0], [1732402800000, 0.0], [1732489200000, 0.0], [1732575600000, 0.0], [1732662000000, 0.0], [1732748400000, 0.0], [1732834800000, 0.0], [1732921200000, 0.00045671], [1733007600000, 0.00068314], [1733094000000, 0.00103581], [1733180400000, 0.00154653], [1733266800000, 0.00203502], [1733353200000, 0.00245059], [1733439600000, 0.0027401], [1733526000000, 0.0027401], [1733612400000, 0.0027401], [1733698800000, 0.0027401], [1733785200000, 0.00331277], [1733871600000, 0.00331277], [1733958000000, 0.00352929], [1734044400000, 0.00352929], [1734130800000, 0.00437782], [1734217200000, 0.00437782], [1734303600000, 0.00437782], [1734390000000, 0.00437782], [1734476400000, 0.00437782], [1734562800000, 0.00437782], [1734649200000, 0.00437782], [1734735600000, 0.00437782], [1734822000000, 0.00483383], [1734908400000, 0.00483383], [1734994800000, 0.00483383], [1735081200000, 0.00483383], [1735167600000, 0.00483383], [1735254000000, 0.00483383], [1735340400000, 0.00688418], [1735426800000, 0.00688418], [1735513200000, 0.00688418], [1735599600000, 0.00688418], [1735686000000, 0.00688418], [1735772400000, 0.00688418], [1735858800000, 0.00688418], [1735945200000, 0.00688418], [1736031600000, 0.00688418], [1736118000000, 0.00688418], [1736204400000, 0.00688418], [1736290800000, 0.00753678], [1736377200000, 0.00753678], [1736463600000, 0.00825444], [1736550000000, 0.00825444], [1736636400000, 0.00825444], [1736722800000, 0.0084866], [1736809200000, 0.0084866], [1736895600000, 0.0084866], [1736982000000, 0.0084866], [1737068400000, 0.0084866], [1737154800000, 0.0084866], [1737241200000, 0.0084866], [1737327600000, 0.0084866], [1737414000000, 0.0084866], [1737500400000, 0.0084866], [1737586800000, 0.0088232], [1737673200000, 0.0088232], [1737759600000, 0.0088232], [1737846000000, 0.0088232], [1737932400000, 0.00947181], [1738018800000, 0.00947181], [1738105200000, 0.00987744]], "nano": [[1732057200000, 0.0], [1732143600000, 0.0], [1732230000000, 0.0], [1732316400000, 0.0], [1732402800000, 0.0], [1732489200000, 0.0], [1732575600000, 0.0], [1732662000000, 0.0], [1732748400000, 0.0], [1732834800000, 0.0], [1732921200000, 0.00045671], [1733007600000, 0.00068314]]};
|
||||
const walletsMetadata = {"nano": {"id": "nano", "address": "0x3837ea82a38daa985ee613e69f72adbe12d0aa50", "name": "Base Wallet", "color": "#58a6ff", "chain": "base"}, "penguin": {"id": "penguin", "address": "0x0c1a4a060e119f981412e323104d1c134d413dba", "name": "Base Wallet", "color": "#f7931a", "chain": "base"}, "cold": {"id": "cold", "address": "bc1qhwsm859uy7aec2h4rj3e00p2vg8nzy8je8duqz", "name": "BTC Cold Storage", "color": "#f7931a", "chain": "btc"}};
|
||||
const walletCosts = {"nano": 57143.88957364997, "penguin": 28214.47038528, "cold": 3528.97661289};
|
||||
const walletBuys = {"nano": 0.5830959900000001, "penguin": 0.29296042999999994, "cold": 0.07518186};
|
||||
const dailySatsData = [{"ts": 1732057200000, "rolling_sats": 0, "btc_val": 0, "price": 94287, "days_since": -10, "cumul_btc": 0}, {"ts": 1732143600000, "rolling_sats": 0, "btc_val": 0, "price": 98317, "days_since": -9, "cumul_btc": 0}, {"ts": 1732230000000, "rolling_sats": 0, "btc_val": 0, "price": 98892, "days_since": -8, "cumul_btc": 0}, {"ts": 1732316400000, "rolling_sats": 0, "btc_val": 0, "price": 97672, "days_since": -7, "cumul_btc": 0}, {"ts": 1732402800000, "rolling_sats": 0, "btc_val": 0, "price": 97900, "days_since": -6, "cumul_btc": 0}, {"ts": 1732489200000, "rolling_sats": 0, "btc_val": 0, "price": 93010, "days_since": -5, "cumul_btc": 0}, {"ts": 1732575600000, "rolling_sats": 0, "btc_val": 0, "price": 91965, "days_since": -4, "cumul_btc": 0}, {"ts": 1732662000000, "rolling_sats": 0, "btc_val": 0, "price": 95863, "days_since": -3, "cumul_btc": 0}, {"ts": 1732748400000, "rolling_sats": 0, "btc_val": 0, "price": 95644, "days_since": -2, "cumul_btc": 0}, {"ts": 1732834800000, "rolling_sats": 0, "btc_val": 0, "price": 97460, "days_since": -1, "cumul_btc": 0}, {"ts": 1732921200000, "rolling_sats": 4152, "btc_val": 0.00004152, "price": 96408, "days_since": 0, "cumul_btc": 0.00045671}, {"ts": 1733007600000, "rolling_sats": 5693, "btc_val": 0.00005693, "price": 97185, "days_since": 1, "cumul_btc": 0.00068314}, {"ts": 1733094000000, "rolling_sats": 7968, "btc_val": 0.00007968, "price": 95841, "days_since": 2, "cumul_btc": 0.00103581}, {"ts": 1733180400000, "rolling_sats": 11047, "btc_val": 0.00011047, "price": 95850, "days_since": 3, "cumul_btc": 0.00154653}, {"ts": 1733266800000, "rolling_sats": 13567, "btc_val": 0.00013567, "price": 98587, "days_since": 4, "cumul_btc": 0.00203502}, {"ts": 1733353200000, "rolling_sats": 15316, "btc_val": 0.00015316, "price": 96946, "days_since": 5, "cumul_btc": 0.00245059}, {"ts": 1733439600000, "rolling_sats": 16118, "btc_val": 0.00016118, "price": 96537, "days_since": 6, "cumul_btc": 0.0027401}, {"ts": 1733526000000, "rolling_sats": 16118, "btc_val": 0.00016118, "price": 96201, "days_since": 7, "cumul_btc": 0.0027401}, {"ts": 1733612400000, "rolling_sats": 16118, "btc_val": 0.00016118, "price": 95811, "days_since": 8, "cumul_btc": 0.0027401}, {"ts": 1733698800000, "rolling_sats": 16118, "btc_val": 0.00016118, "price": 97183, "days_since": 9, "cumul_btc": 0.0027401}, {"ts": 1733785200000, "rolling_sats": 19612, "btc_val": 0.00019612, "price": 96685, "days_since": 10, "cumul_btc": 0.00331277}, {"ts": 1733871600000, "rolling_sats": 19612, "btc_val": 0.00019612, "price": 97491, "days_since": 11, "cumul_btc": 0.00331277}, {"ts": 1733958000000, "rolling_sats": 20414, "btc_val": 0.00020414, "price": 96537, "days_since": 12, "cumul_btc": 0.00352929}, {"ts": 1734044400000, "rolling_sats": 20414, "btc_val": 0.00020414, "price": 96685, "days_since": 13, "cumul_btc": 0.00352929}, {"ts": 1734130800000, "rolling_sats": 22742, "btc_val": 0.00022742, "price": 97163, "days_since": 14, "cumul_btc": 0.00437782}, {"ts": 1734217200000, "rolling_sats": 22742, "btc_val": 0.00022742, "price": 97000, "days_since": 15, "cumul_btc": 0.00437782}];
|
||||
const aaveCumulData = [[1732057200000, 0], [1732143600000, 0], [1732230000000, 0], [1732316400000, 0], [1732402800000, 0], [1732489200000, 0], [1732575600000, 0], [1732662000000, 0], [1732748400000, 0], [1732834800000, 0], [1732921200000, 0], [1733007600000, 0], [1733094000000, 0], [1733180400000, 0], [1733266800000, 0], [1733353200000, 0]];
|
||||
const directCumulData = [[1732057200000, 0], [1732143600000, 0], [1732230000000, 0], [1732316400000, 0], [1732402800000, 0], [1732489200000, 0], [1732575600000, 0], [1732662000000, 0], [1732748400000, 0], [1732834800000, 0], [1732921200000, 0.00045671], [1733007600000, 0.00068314]];
|
||||
const totalCumulData = [[1732057200000, 0], [1732143600000, 0], [1732230000000, 0], [1732316400000, 0], [1732402800000, 0], [1732489200000, 0], [1732575600000, 0], [1732662000000, 0], [1732748400000, 0], [1732834800000, 0], [1732921200000, 0.00045671], [1733007600000, 0.00068314]];
|
||||
|
||||
/* ===================================================================
|
||||
App State
|
||||
=================================================================== */
|
||||
@ -401,7 +410,6 @@ let tickIntervalId;
|
||||
|
||||
/* Aave data */
|
||||
let aavePriceMap = {};
|
||||
let allAaveSnapshots = [];
|
||||
let lastFetchMs = 0;
|
||||
let todayBtcPrice = null;
|
||||
|
||||
@ -411,21 +419,11 @@ const walletSyncState = {};
|
||||
/* Per-address snapshot data, keyed by address */
|
||||
const addressSnapshots = {};
|
||||
|
||||
/* Which embedded wallet IDs each tracked address maps to */
|
||||
function findEmbeddedId(address) {
|
||||
for (const [id, meta] of Object.entries(walletsMetadata)) {
|
||||
if (meta.address.toLowerCase() === address.toLowerCase()) return id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Assigned color index for dynamic wallets */
|
||||
let _colorIdx = 0;
|
||||
function getColorForWallet(address) {
|
||||
const c = localStorage.getItem('cbbtc_color_' + address);
|
||||
if (c) return c;
|
||||
const embedded = findEmbeddedId(address);
|
||||
if (embedded && walletsMetadata[embedded].color) return walletsMetadata[embedded].color;
|
||||
const color = WALLET_COLORS[_colorIdx % WALLET_COLORS.length];
|
||||
_colorIdx++;
|
||||
localStorage.setItem('cbbtc_color_' + address, color);
|
||||
@ -582,16 +580,27 @@ function renderSidebar() {
|
||||
? `<span class="text-yellow-400 text-[10px] ml-2">⏳ Syncing</span>`
|
||||
: '';
|
||||
const nick = w.nickname || shortAddr;
|
||||
const isEVM = !['btc', 'bitcoin', 'solana'].includes(w.chain);
|
||||
const isVerified = w.isVerified;
|
||||
const verifyBadge = isEVM
|
||||
? (isVerified ? '<span class="text-green-400 text-[10px] ml-2">✓ Verified</span>' : '<span class="text-red-400 text-[10px] ml-2">⚠ Unverified</span>')
|
||||
: '<span class="text-blue-400 text-[10px] ml-2">🔒 No-Sig</span>';
|
||||
const verifyBtn = isEVM && !isVerified
|
||||
? '<button onclick="handleVerifyWallet(\'' + w.address + '\',\'' + w.chain + '\',\'' + nick + '\')" class="text-[10px] bg-[#FF7A00]/20 hover:bg-[#FF7A00]/40 text-[#FF7A00] font-bold px-2 py-0.5 rounded transition" title="Verify ownership via MetaMask">Verify</button>'
|
||||
: '';
|
||||
const revokeBtn = isVerified && isEVM
|
||||
? '<button onclick="handleRevokeVerification(\'' + w.address + '\',\'' + w.chain + '\')" class="text-[10px] bg-red-500/20 hover:bg-red-500/40 text-red-400 font-bold px-2 py-0.5 rounded transition" title="Revoke verification">Revoke</button>'
|
||||
: '';
|
||||
html += '<div class="flex items-center justify-between bg-[#05070B] border border-[#1A1F2C]/40 rounded-lg px-3 py-2.5">' +
|
||||
'<div class="flex items-center gap-2.5 min-w-0 flex-1">' +
|
||||
'<div onclick="openColorPicker(event, \'' + w.address + '\')" class="wallet-color-dot" style="--wallet-color:' + color + ';background:' + color + ';" title="Click to change color"></div>' +
|
||||
'<div class="min-w-0 flex-1">' +
|
||||
'<input class="rename-nick-input bg-transparent text-sm font-medium text-white truncate border border-transparent focus:border-[#FF7A00]/50 rounded px-1 outline-none transition block w-full" value="' + nick + '" data-addr="' + w.address + '" data-chain="' + w.chain + '" onblur="handleRenameNickname(this)" onkeydown="if(event.key===\'Enter\')this.blur()">' +
|
||||
'<div class="text-[10px] text-gray-500 font-mono truncate">' + shortAddr + '</div>' +
|
||||
'<div class="text-[10px] text-gray-500 font-mono truncate">' + shortAddr + ' <span class="text-gray-600">' + w.chain + '</span>' + verifyBadge + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="flex items-center gap-1 flex-shrink-0">' +
|
||||
syncBadge +
|
||||
verifyBtn + revokeBtn + syncBadge +
|
||||
'<button onclick="handleDeleteWallet(\'' + w.address + '\',\'' + w.chain + '\',' + i + ')" class="text-gray-600 hover:text-red-400 transition" title="Remove wallet">' +
|
||||
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
||||
'<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>' +
|
||||
@ -627,17 +636,42 @@ function closeAddWalletModal() {
|
||||
|
||||
async function handleAddWallet(e) {
|
||||
e.preventDefault();
|
||||
const address = document.getElementById('input-address').value.trim();
|
||||
const address = document.getElementById('input-address').value.trim().toLowerCase();
|
||||
const nickname = document.getElementById('input-nickname').value.trim();
|
||||
const errEl = document.getElementById('add-wallet-error');
|
||||
|
||||
if (!address) { errEl.textContent = 'Address is required'; errEl.classList.remove('hidden'); return false; }
|
||||
if (!/^(0x)?[0-9a-fA-F]{40}$/i.test(address)) { errEl.textContent = 'Invalid EVM address format'; errEl.classList.remove('hidden'); return false; }
|
||||
|
||||
/* Register in WalletManager */
|
||||
/* Detect chain from address format */
|
||||
let chain = 'base';
|
||||
const isEVM = /^(0x)?[0-9a-fA-F]{40}$/i.test(address);
|
||||
const isBTC = /^(bc1[a-z0-9]{25,39}|1[a-km-zA-HJ-NP-Z1-9]{25,34})$/i.test(address);
|
||||
// if (!isEVM && !isBTC) { errEl.textContent = 'Invalid address format'; errEl.classList.remove('hidden'); return false; }
|
||||
|
||||
if (!isEVM && !isBTC) chain = 'btc';
|
||||
|
||||
/* Auto-verify non-EVM wallets */
|
||||
if (!isEVM && isBTC) {
|
||||
const addResult = wm.addWallet(address, 'btc', nickname);
|
||||
if (!addResult.success) { errEl.textContent = addResult.error; errEl.classList.remove('hidden'); return false; }
|
||||
wm.verifyWallet(address, 'btc', '', { action: 'Auto-verified (non-verification chain)', walletAddress: address });
|
||||
walletSyncState[address] = 'synced';
|
||||
closeAddWalletModal();
|
||||
renderAll();
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Register EVM wallet */
|
||||
const addResult = wm.addWallet(address, 'base', nickname);
|
||||
if (!addResult.success) { errEl.textContent = addResult.error; errEl.classList.remove('hidden'); return false; }
|
||||
|
||||
/* Attempt EIP-712 verification if MetaMask is available */
|
||||
const verifyResult = await verifyOwnership(address, nickname);
|
||||
if (!verifyResult.success) {
|
||||
console.log("Verification not performed:", verifyResult.error);
|
||||
/* Wallet saved but unverified — user can verify from sidebar */
|
||||
}
|
||||
|
||||
/* POST to FastAPI backend to start monitoring */
|
||||
try {
|
||||
const resp = await fetch('http://localhost:8000/api/v1/portfolio/monitor', {
|
||||
@ -646,12 +680,10 @@ async function handleAddWallet(e) {
|
||||
body: JSON.stringify({ address: address, chain: 'base', chain_id: 8453 })
|
||||
});
|
||||
if (!resp.ok) {
|
||||
errEl.textContent = `Backend returned ${resp.status}. Wallet saved locally.`;
|
||||
errEl.classList.remove('hidden');
|
||||
console.log(`Backend returned ${resp.status}`);
|
||||
}
|
||||
} catch (err) {
|
||||
errEl.textContent = 'Backend unavailable. Wallet saved locally.';
|
||||
errEl.classList.remove('hidden');
|
||||
console.error('Backend unavailable', err);
|
||||
}
|
||||
|
||||
/* Mark as syncing initially */
|
||||
@ -668,6 +700,81 @@ async function handleAddWallet(e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
EIP-712 Verification
|
||||
=================================================================== */
|
||||
const EIP712_DOMAIN = { name: "Anonymous Wallet Tracker", version: "1", chainId: 1 };
|
||||
const VERIFY_TYPES = {
|
||||
VerifyTracking: [
|
||||
{ name: 'action', type: 'string' },
|
||||
{ name: 'walletAddress', type: 'address' },
|
||||
{ name: 'timestamp', type: 'uint256' }
|
||||
]
|
||||
};
|
||||
|
||||
async function verifyOwnership(address, nickname) {
|
||||
if (!window.ethereum) {
|
||||
return { success: false, error: 'No web3 provider' };
|
||||
}
|
||||
const message = {
|
||||
action: 'Verify Ownership for Live Tracking',
|
||||
walletAddress: address,
|
||||
timestamp: Math.floor(Date.now() / 1000).toString()
|
||||
};
|
||||
const typedData = {
|
||||
domain: EIP712_DOMAIN,
|
||||
types: { EIP712Domain: [{ name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }], VerifyTracking: VERIFY_TYPES.VerifyTracking },
|
||||
primaryType: 'VerifyTracking',
|
||||
message
|
||||
};
|
||||
try {
|
||||
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||
if (!accounts || accounts.length === 0) return { success: false, error: 'No account' };
|
||||
|
||||
/* Require that the connected signer matches the added wallet */
|
||||
if (accounts[0].toLowerCase() !== address) {
|
||||
return { success: false, error: 'Connected wallet does not match added address' };
|
||||
}
|
||||
|
||||
const sig = await window.ethereum.request({
|
||||
method: 'eth_signTypedData_v4',
|
||||
params: [accounts[0], JSON.stringify(typedData)]
|
||||
});
|
||||
wm.verifyWallet(address, 'base', sig, typedData.message);
|
||||
wm.renameWallet(address, 'base', nickname);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return { success: false, error: err?.message || 'Signature failed' };
|
||||
}
|
||||
}
|
||||
|
||||
async function handleVerifyWallet(address, chain, nickname) {
|
||||
const w = wm.findWallet(address, chain);
|
||||
if (!w) return;
|
||||
if (w.isVerified) return;
|
||||
|
||||
/* Non-EVM auto-verify */
|
||||
if (['btc', 'bitcoin', 'solana'].includes(chain)) {
|
||||
wm.verifyWallet(address, chain, '', { action: 'Auto-verified (non-verification chain)', walletAddress: address });
|
||||
renderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await verifyOwnership(address, nickname || w.nickname);
|
||||
if (result.success) {
|
||||
/* Fetch data now that wallet is verified */
|
||||
if (chain === 'base') fetchWalletAaveData(address);
|
||||
} else {
|
||||
alert('Verification failed: ' + result.error);
|
||||
}
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function handleRevokeVerification(address, chain) {
|
||||
wm.revokeVerification(address, chain);
|
||||
renderAll();
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
Delete Wallet
|
||||
=================================================================== */
|
||||
@ -805,8 +912,6 @@ async function fetchSelectedWalletsData() {
|
||||
/* ===================================================================
|
||||
Chart Helpers
|
||||
=================================================================== */
|
||||
const penguinAddress = '0x0c1a4a060e119f981412e323104d1c134d413dba';
|
||||
|
||||
function getTokenAmount(raw, symbol) {
|
||||
const dec = TOKENS[symbol] ? TOKENS[symbol].decimals : 18;
|
||||
return parseFloat(raw || '0') / Math.pow(10, dec);
|
||||
@ -886,45 +991,46 @@ function getOldestTransactionDate() {
|
||||
if (sTs < oldestTs) oldestTs = sTs;
|
||||
});
|
||||
});
|
||||
if (selectedAddr.map(a => a.toLowerCase()).includes(penguinAddress.toLowerCase())) {
|
||||
allAaveSnapshots.forEach(snap => {
|
||||
const sTs = new Date(snap.block_timestamp).getTime();
|
||||
if (sTs < oldestTs) oldestTs = sTs;
|
||||
});
|
||||
}
|
||||
return oldestTs === Infinity ? Date.now() - 365 * 86400000 : oldestTs;
|
||||
}
|
||||
|
||||
/* Build per-day cumulative BTC series from addressSnapshots */
|
||||
function buildCumulativeSeries(addresses) {
|
||||
const dayMap = {};
|
||||
addresses.forEach(addr => {
|
||||
const snaps = addressSnapshots[addr] || [];
|
||||
snaps.forEach(snap => {
|
||||
const ts = new Date(snap.block_timestamp).getTime();
|
||||
const dateStr = snap.block_timestamp.slice(0, 10);
|
||||
const btcAmt = getTokenAmount(snap?.wallet?.cbBTC, 'cbBTC') + getTokenAmount(snap?.collateral?.cbBTC, 'cbBTC');
|
||||
if (!dayMap[ts]) dayMap[ts] = { total: 0, dateStr };
|
||||
dayMap[ts].total += btcAmt;
|
||||
});
|
||||
});
|
||||
const sorted = Object.values(dayMap).sort((a, b) => {
|
||||
const tsA = new Date(a.dateStr).getTime();
|
||||
const tsB = new Date(b.dateStr).getTime();
|
||||
return tsA - tsB;
|
||||
});
|
||||
let cumul = 0;
|
||||
return sorted.map(d => {
|
||||
cumul += d.total;
|
||||
const dateStr = d.dateStr;
|
||||
return [new Date(dateStr).getTime(), cumul];
|
||||
});
|
||||
}
|
||||
|
||||
function calculateAggregatedSeries() {
|
||||
const selectedAddr = getSelectedAddresses();
|
||||
const activeWalletIds = selectedAddr.map(a => findEmbeddedId(a)).filter(Boolean);
|
||||
const uniqueIds = [...new Set(activeWalletIds)];
|
||||
|
||||
currentBuyCost = 0;
|
||||
currentBuyAmount = 0;
|
||||
|
||||
if (uniqueIds.length === 0) {
|
||||
if (selectedAddr.length === 0) {
|
||||
currentAggregatedSeries = [];
|
||||
currentNetHeld = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const firstWid = uniqueIds[0];
|
||||
const baseSeries = walletCumulData[firstWid] || [];
|
||||
const result = baseSeries.map(point => [point[0], 0]);
|
||||
|
||||
uniqueIds.forEach(wid => {
|
||||
const series = walletCumulData[wid];
|
||||
if (series) {
|
||||
series.forEach((point, i) => {
|
||||
if (result[i]) result[i][1] += point[1];
|
||||
});
|
||||
}
|
||||
currentBuyCost += walletCosts[wid] || 0;
|
||||
currentBuyAmount += walletBuys[wid] || 0;
|
||||
});
|
||||
|
||||
currentAggregatedSeries = result;
|
||||
currentAggregatedSeries = buildCumulativeSeries(selectedAddr);
|
||||
}
|
||||
|
||||
function calculateCurrentHoldings() {
|
||||
@ -934,32 +1040,32 @@ function calculateCurrentHoldings() {
|
||||
currentBuyAmount = 0;
|
||||
if (selectedAddr.length === 0) return;
|
||||
|
||||
const hasPenguinSnapshots = selectedAddr.map(a => a.toLowerCase()).includes(penguinAddress.toLowerCase()) && allAaveSnapshots.length > 0;
|
||||
let hasSnapshotData = false;
|
||||
if (hasPenguinSnapshots) {
|
||||
const latest = allAaveSnapshots[0];
|
||||
const cold = getTokenAmount(latest?.wallet?.cbBTC, 'cbBTC');
|
||||
const collat = getTokenAmount(latest?.collateral?.cbBTC, 'cbBTC');
|
||||
currentNetHeld += cold + collat;
|
||||
hasSnapshotData = true;
|
||||
}
|
||||
|
||||
/* Also aggregate from dynamic snapshots */
|
||||
/* Sum latest cbBTC balance across selected wallets */
|
||||
selectedAddr.forEach(addr => {
|
||||
const embeddedId = findEmbeddedId(addr);
|
||||
if (addr.toLowerCase() === penguinAddress.toLowerCase() && hasSnapshotData) return;
|
||||
const snaps = addressSnapshots[addr];
|
||||
if (snaps && snaps.length > 0) {
|
||||
const snaps = addressSnapshots[addr] || [];
|
||||
if (snaps.length > 0) {
|
||||
const latest = snaps[0];
|
||||
currentNetHeld += getTokenAmount(latest?.wallet?.cbBTC, 'cbBTC');
|
||||
currentNetHeld += getTokenAmount(latest?.collateral?.cbBTC, 'cbBTC');
|
||||
}
|
||||
if (embeddedId && !hasSnapshotData) {
|
||||
const series = walletCumulData[embeddedId];
|
||||
if (series && series.length > 0) currentNetHeld += series[series.length - 1][1];
|
||||
currentBuyCost += walletCosts[embeddedId] || 0;
|
||||
currentBuyAmount += walletBuys[embeddedId] || 0;
|
||||
}
|
||||
});
|
||||
|
||||
/* Derive buy cost from snapshot deltas: positive cbBTC changes across days */
|
||||
selectedAddr.forEach(addr => {
|
||||
const snaps = addressSnapshots[addr] || [];
|
||||
if (snaps.length < 2) return;
|
||||
let prevBtc = 0;
|
||||
snaps.forEach(snap => {
|
||||
const currentDate = snap.block_timestamp.slice(0, 10);
|
||||
const currentBtc = getTokenAmount(snap?.wallet?.cbBTC, 'cbBTC') + getTokenAmount(snap?.collateral?.cbBTC, 'cbBTC');
|
||||
const delta = currentBtc - prevBtc;
|
||||
if (delta > 0) {
|
||||
const price = priceForToken('cbBTC', currentDate) || 0;
|
||||
if (price > 0) currentBuyCost += delta * price;
|
||||
currentBuyAmount += delta;
|
||||
}
|
||||
prevBtc = currentBtc;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -1040,22 +1146,9 @@ function renderCombinedTable() {
|
||||
walletColorMap[w.address] = getColorForWallet(w.address);
|
||||
walletNickMap[w.address] = w.nickname || 'Wallet';
|
||||
});
|
||||
walletsMetadata && Object.values(walletsMetadata).forEach(m => {
|
||||
walletColorMap[m.address] = m.color || '#f7931a';
|
||||
walletNickMap[m.address] = m.name;
|
||||
});
|
||||
|
||||
/* Build unified snapshot list from all sources */
|
||||
/* Build unified snapshot list from dynamic sources only */
|
||||
const unified = [];
|
||||
|
||||
/* Penguin (embedded) snapshots */
|
||||
if (allAaveSnapshots.length > 0) {
|
||||
allAaveSnapshots.forEach(snap => {
|
||||
unified.push({ ...snap, _walletAddress: penguinAddress });
|
||||
});
|
||||
}
|
||||
|
||||
/* Dynamic snapshots */
|
||||
for (const [addr, snaps] of Object.entries(addressSnapshots)) {
|
||||
(snaps || []).forEach(snap => {
|
||||
unified.push({ ...snap, _walletAddress: addr });
|
||||
@ -1166,20 +1259,16 @@ function renderCombinedTable() {
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
Fetch Aave Snapshots (for embedded wallets)
|
||||
Fetch Portfolio Data for All Verified Wallets
|
||||
=================================================================== */
|
||||
async function fetchAaveSnapshots() {
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/api/v1/portfolio/${penguinAddress}/base/aave`);
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const events = await resp.json();
|
||||
const oldestTs = new Date(events[0].block_timestamp).getTime();
|
||||
async function fetchAllWalletData() {
|
||||
const verifiedWallets = wm.getVerifiedWallets() || [];
|
||||
const promises = verifiedWallets.map(async (w) => fetchWalletAaveData(w.address));
|
||||
await Promise.all(promises);
|
||||
const allSnaps = Object.values(addressSnapshots).flat();
|
||||
if (allSnaps.length > 0) {
|
||||
const oldestTs = new Date(allSnaps[0].block_timestamp).getTime();
|
||||
await fetchPrices(Object.keys(TOKENS), oldestTs);
|
||||
allAaveSnapshots = snapshotsToDaily(events);
|
||||
addressSnapshots[penguinAddress] = allAaveSnapshots;
|
||||
walletSyncState[penguinAddress] = 'synced';
|
||||
} catch (err) {
|
||||
console.error("Failed to load aave data:", err);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1258,8 +1347,22 @@ function setupCumulCard(cutoff) {
|
||||
}
|
||||
|
||||
function setupSatsCard(cutoff) {
|
||||
const rawFiltered = dailySatsData.filter(d => d.ts >= cutoff);
|
||||
const rollingAvg = rawFiltered.map(d => [d.ts, Math.round(d.rolling_sats)]);
|
||||
/* Derive daily BTC from all verified wallet snapshots */
|
||||
const dailyBtc = {};
|
||||
const selectedAddr = getSelectedAddresses() || [];
|
||||
selectedAddr.forEach(addr => {
|
||||
const snaps = addressSnapshots[addr] || [];
|
||||
snaps.forEach(snap => {
|
||||
const ts = new Date(snap.block_timestamp).getTime();
|
||||
const dateStr = snap.block_timestamp.slice(0, 10);
|
||||
const btcVal = getTokenAmount(snap?.wallet?.cbBTC, 'cbBTC') + getTokenAmount(snap?.collateral?.cbBTC, 'cbBTC');
|
||||
if (!dailyBtc[ts]) dailyBtc[ts] = { ts, btcVal: 0, dateStr };
|
||||
dailyBtc[ts].btcVal += btcVal;
|
||||
});
|
||||
});
|
||||
const dailyEntries = Object.values(dailyBtc).sort((a, b) => a.ts - b.ts);
|
||||
const filtered = dailyEntries.filter(d => d.ts >= cutoff);
|
||||
const rollingAvg = filtered.map(d => [d.ts, Math.round(d.btcVal * 1e8)]);
|
||||
const latestAvg = rollingAvg.length > 0 ? rollingAvg[rollingAvg.length - 1][1] : 0;
|
||||
document.getElementById('avg-sats-val').innerText = new Intl.NumberFormat('en-US').format(Math.round(latestAvg));
|
||||
const options = {
|
||||
@ -1273,7 +1376,7 @@ function setupSatsCard(cutoff) {
|
||||
markers: { size: 0, hover: { size: 5 } },
|
||||
xaxis: { type: 'datetime', labels: { show: true, style: { colors: '#4B5563', fontSize: '10px', fontWeight: 600 }, datetimeFormatter: { year: 'yyyy', month: 'MMM yy', day: 'dd MMM' } }, axisBorder: { show: false }, axisTicks: { show: false } },
|
||||
yaxis: { opposite: true, labels: { offsetX: -10, style: { colors: '#4B5563', fontSize: '10px', fontWeight: 600 }, formatter: (val) => { if (val >= 1e6) return (val / 1e6).toFixed(0) + 'M'; if (val >= 1e3) return (val / 1e3).toFixed(0) + 'k'; return val; } }, axisBorder: { show: false }, axisTicks: { show: false } },
|
||||
tooltip: { enabled: true, theme: 'dark', shared: false, intersect: false, custom: function({ series, seriesIndex, dataPointIndex, w }) { const item = rawFiltered[dataPointIndex]; if (!item) return ''; const date = new Date(item.ts); const dateString = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); const btcVal = parseFloat(item.btc_val).toFixed(4); const price = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(item.price); const dollarDay = parseFloat(item.btc_val) * item.price; const dollarFormatted = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(dollarDay); return '<div class="rounded-lg py-2 px-4 text-left"><div class="text-gray-400 text-[10px] font-semibold uppercase tracking-tighter mb-3">' + dateString + '</div><div class="mb-3"><div class="text-orange-400 text-xs font-bold uppercase mb-1">₿ ' + btcVal + '/day</div><div class="text-gray-300 text-xs">Dollar Amount/Day</div><div class="text-white text-lg font-medium">' + dollarFormatted + '</div></div><div class="border-t border-gray-600/50 pt-2.5"><div class="text-gray-400 text-[10px] uppercase mb-1">Calculation</div><div class="text-gray-300 text-xs">₿ ' + btcVal + ' × ' + price + '</div></div></div>'; } },
|
||||
tooltip: { enabled: true, theme: 'dark', shared: false, intersect: false, custom: function({ series, seriesIndex, dataPointIndex, w }) { const rawData = w.config.series[seriesIndex].data[dataPointIndex]; if (!rawData) return ''; const date = new Date(rawData[0]); const dateString = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); const sats = rawData[1]; const btcVal = (sats / 1e8).toFixed(6); const priceStr = todayBtcPrice ? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(todayBtcPrice) : '—'; const dollarDay = todayBtcPrice ? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format((sats / 1e8) * todayBtcPrice) : '—'; return '<div class="rounded-lg py-2 px-4 text-left"><div class="text-gray-400 text-[10px] font-semibold uppercase tracking-tighter mb-3">' + dateString + '</div><div class="text-orange-400 text-xs font-bold">₿ ' + btcVal + '/day</div><div class="text-white text-sm font-medium mt-1">' + dollarDay + '</div></div>'; } },
|
||||
crosshairs: { show: true, width: 1, position: 'back', stroke: { color: '#4B5563', width: 1, dashArray: 3 } }
|
||||
};
|
||||
const chart = new ApexCharts(document.querySelector("#chart-container-sats"), options);
|
||||
@ -1282,11 +1385,35 @@ function setupSatsCard(cutoff) {
|
||||
}
|
||||
|
||||
function setupBreakdownCard(cutoff) {
|
||||
/* Derive from snapshots: cold wallet vs collateral */
|
||||
const dayMap = {};
|
||||
const selectedAddr = getSelectedAddresses() || [];
|
||||
selectedAddr.forEach(addr => {
|
||||
const snaps = addressSnapshots[addr] || [];
|
||||
snaps.forEach(snap => {
|
||||
const ts = new Date(snap.block_timestamp).getTime();
|
||||
if (!dayMap[ts]) dayMap[ts] = { cold: 0, collateral: 0 };
|
||||
dayMap[ts].cold += getTokenAmount(snap?.wallet?.cbBTC, 'cbBTC');
|
||||
dayMap[ts].collateral += getTokenAmount(snap?.collateral?.cbBTC, 'cbBTC');
|
||||
});
|
||||
});
|
||||
const entries = Object.keys(dayMap).map(ts => parseInt(ts)).sort((a, b) => a - b);
|
||||
let c0 = 0, c1 = 0;
|
||||
const totalSeries = entries.map(ts => {
|
||||
c0 += dayMap[ts].cold + dayMap[ts].collateral;
|
||||
return [ts, c0];
|
||||
}).filter(d => d[0] >= cutoff);
|
||||
let c2 = 0;
|
||||
const aaveSeries = entries.map(ts => {
|
||||
c2 += dayMap[ts].collateral;
|
||||
return [ts, c2];
|
||||
}).filter(d => d[0] >= cutoff);
|
||||
|
||||
const opts = {
|
||||
chart: { id: 'instance-breakdown', type: 'area', height: 200, background: 'transparent', toolbar: { show: false }, sparkline: { enabled: false }, zoom: { enabled: true, type: 'x', autoScaleYaxis: true }, animations: { enabled: false } },
|
||||
series: [
|
||||
{ name: 'Total Holdings', data: totalCumulData.filter(d => d[0] >= cutoff) },
|
||||
{ name: 'On Aave', data: aaveCumulData.filter(d => d[0] >= cutoff) }
|
||||
{ name: 'Total Holdings', data: totalSeries },
|
||||
{ name: 'On Aave', data: aaveSeries }
|
||||
],
|
||||
dataLabels: { enabled: false },
|
||||
colors: [orangeBrandColor, blueBrandColor],
|
||||
@ -1296,7 +1423,7 @@ function setupBreakdownCard(cutoff) {
|
||||
markers: { size: 0, strokeColors: [orangeBrandColor, blueBrandColor], strokeWidth: 3, hover: { size: 5 } },
|
||||
xaxis: { type: 'datetime', labels: { show: true, style: { colors: '#4B5563', fontSize: '10px', fontWeight: 600 }, datetimeFormatter: { year: 'yyyy', month: 'MMM yy', day: 'dd MMM' } }, axisBorder: { show: false }, axisTicks: { show: false } },
|
||||
yaxis: { opposite: true, labels: { offsetX: -10, style: { colors: '#4B5563', fontSize: '10px', fontWeight: 600 }, formatter: (val) => val.toFixed(4) }, axisBorder: { show: false }, axisTicks: { show: false } },
|
||||
tooltip: { enabled: true, theme: 'dark', shared: true, intersect: false, custom: function({ series, seriesIndex, dataPointIndex, w }) { const item = totalCumulData.filter(d => d[0] >= cutoff)[dataPointIndex]; if (!item) return ''; const date = new Date(item[0]); const dateString = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); const total = item[1]; const aave = aaveCumulData.filter(d => d[0] >= cutoff)[dataPointIndex]?.[1] || 0; const delta = total - aave; return '<div class="rounded-lg py-2 px-4 text-left"><div class="text-gray-400 text-[10px] font-semibold uppercase tracking-tighter mb-3">' + dateString + '</div><div class="flex items-center justify-between mb-2"><div class="flex items-center gap-1.5"><div class="w-2.5 h-2.5 rounded-full" style="background-color:' + orangeBrandColor + ';"></div><span class="text-gray-300 text-xs">Total</span></div><span class="text-orange-400 text-sm font-bold">' + total.toFixed(6) + '</span></div><div class="flex items-center justify-between mb-2"><div class="flex items-center gap-1.5"><div class="w-2.5 h-2.5 rounded-full" style="background-color:' + blueBrandColor + ';"></div><span class="text-gray-300 text-xs">On Aave</span></div><span class="text-blue-400 text-sm font-bold">' + aave.toFixed(6) + '</span></div><div class="flex items-center justify-between"><div class="flex items-center gap-1.5"><div class="w-2.5 h-2.5 rounded-full" style="background-color: #22c55e;"></div><span class="text-gray-300 text-xs">Delta</span></div><span class="text-green-400 text-sm font-bold">' + delta.toFixed(6) + '</span></div></div>'; } },
|
||||
tooltip: { enabled: true, theme: 'dark', shared: true, intersect: false, custom: function({ series, seriesIndex, dataPointIndex, w }) { const item = totalSeries[dataPointIndex]; if (!item) return ''; const date = new Date(item[0]); const dateString = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); const total = item[1]; const aave = aaveSeries[dataPointIndex]?.[1] || 0; const delta = total - aave; return '<div class="rounded-lg py-2 px-4 text-left"><div class="text-gray-400 text-[10px] font-semibold uppercase tracking-tighter mb-3">' + dateString + '</div><div class="flex items-center justify-between mb-2"><div class="flex items-center gap-1.5"><div class="w-2.5 h-2.5 rounded-full" style="background-color:' + orangeBrandColor + ';"></div><span class="text-gray-300 text-xs">Total</span></div><span class="text-orange-400 text-sm font-bold">' + total.toFixed(6) + '</span></div><div class="flex items-center justify-between mb-2"><div class="flex items-center gap-1.5"><div class="w-2.5 h-2.5 rounded-full" style="background-color:' + blueBrandColor + ';"></div><span class="text-gray-300 text-xs">On Aave</span></div><span class="text-blue-400 text-sm font-bold">' + aave.toFixed(6) + '</span></div><div class="flex items-center justify-between"><div class="flex items-center gap-1.5"><div class="w-2.5 h-2.5 rounded-full" style="background-color: #22c55e;"></div><span class="text-gray-300 text-xs">Delta</span></div><span class="text-green-400 text-sm font-bold">' + delta.toFixed(6) + '</span></div></div>'; } },
|
||||
legend: { position: 'top', horizontalAlign: 'left', fontSize: '10px', fontFamily: 'sans-serif', labels: { colors: '#4B5563' }, markers: { width: 8, height: 8, radius: 12, offsetX: -4 }, itemMargin: { horizontal: 8, vertical: 0 } },
|
||||
crosshairs: { show: true, width: 1, position: 'back', stroke: { color: '#4B5563', width: 1, dashArray: 3 } }
|
||||
};
|
||||
@ -1367,19 +1494,51 @@ function startSyncPolling() {
|
||||
setInterval(() => pollSyncStatus(), 15000);
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
Polling for Verified Wallets
|
||||
=================================================================== */
|
||||
async function pollVerifiedWallets() {
|
||||
const now = Date.now();
|
||||
if (now - lastFetchMs < 30000) return;
|
||||
lastFetchMs = now;
|
||||
const verified = wm.getVerifiedWallets() || [];
|
||||
for (const w of verified) {
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/api/v1/portfolio/${w.address}/base/aave`);
|
||||
if (!resp.ok) continue;
|
||||
const events = await resp.json();
|
||||
const newestTs = events[events.length - 1].block_timestamp;
|
||||
const existing = addressSnapshots[w.address];
|
||||
if (existing && existing.length > 0) {
|
||||
if (newestTs > existing[0].block_timestamp) {
|
||||
addressSnapshots[w.address] = snapshotsToDaily(events);
|
||||
walletSyncState[w.address] = 'synced';
|
||||
}
|
||||
} else {
|
||||
addressSnapshots[w.address] = snapshotsToDaily(events);
|
||||
walletSyncState[w.address] = 'synced';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Poll update failed for " + w.address, err);
|
||||
}
|
||||
}
|
||||
const anyUpdated = Object.keys(addressSnapshots).length > 0;
|
||||
if (anyUpdated) {
|
||||
await refreshPrices();
|
||||
renderCombinedTable();
|
||||
updateDashboard();
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
setInterval(() => pollVerifiedWallets(), 60000);
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
Initialization
|
||||
=================================================================== */
|
||||
async function initDashboardGrid() {
|
||||
/* Seed penguin in localStorage if not present already */
|
||||
if (!wm.findWallet(penguinAddress, 'base')) {
|
||||
wm.addWallet(penguinAddress, 'base', 'penguin');
|
||||
walletSyncState[penguinAddress] = 'syncing';
|
||||
/* Auto-select the default wallet */
|
||||
localStorage.setItem('cbbtc_selected', JSON.stringify([penguinAddress]));
|
||||
}
|
||||
|
||||
renderWalletPills();
|
||||
renderWalletPills();
|
||||
renderLedgerFilterWallets();
|
||||
|
||||
try {
|
||||
@ -1389,11 +1548,8 @@ async function initDashboardGrid() {
|
||||
const rawPrices = result.result.XXBTZUSD;
|
||||
btcPriceData = rawPrices.map(item => [item[0] * 1000, parseFloat(item[4])]);
|
||||
|
||||
/* Fetch embedded wallet data */
|
||||
await fetchAaveSnapshots();
|
||||
|
||||
/* Fetch data for all selected wallets */
|
||||
await fetchSelectedWalletsData();
|
||||
/* Fetch data for all verified wallets */
|
||||
await fetchAllWalletData();
|
||||
|
||||
await refreshPrices();
|
||||
renderCombinedTable();
|
||||
@ -1409,31 +1565,6 @@ async function initDashboardGrid() {
|
||||
}
|
||||
}
|
||||
|
||||
async function pollAaveUpdate() {
|
||||
const now = Date.now();
|
||||
if (now - lastFetchMs < 30000) return;
|
||||
lastFetchMs = now;
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/api/v1/portfolio/${penguinAddress}/base/aave`);
|
||||
if (!resp.ok) return;
|
||||
const events = await resp.json();
|
||||
lastFetchMs = Date.now();
|
||||
const newestTs = events[events.length - 1].block_timestamp;
|
||||
if (newestTs > (allAaveSnapshots[0]?.block_timestamp || '')) {
|
||||
allAaveSnapshots = snapshotsToDaily(events);
|
||||
addressSnapshots[penguinAddress] = allAaveSnapshots;
|
||||
await refreshPrices();
|
||||
renderCombinedTable();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Poll update failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
setInterval(() => pollAaveUpdate(), 60000);
|
||||
}
|
||||
|
||||
initDashboardGrid();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
40
wallets.js
40
wallets.js
@ -141,19 +141,19 @@ export class WalletManager {
|
||||
return [];
|
||||
}
|
||||
|
||||
/* Sanitize: enforce isVerified = false for all persisted entries.
|
||||
A wallet can only be verified by calling verifyWallet(), not
|
||||
by tampering with localStorage. */
|
||||
this._wallets = parsed
|
||||
.filter((w) => w && typeof w === 'object' && w.address && w.chain)
|
||||
.map((w) => ({
|
||||
address: String(w.address),
|
||||
chain: String(w.chain).toLowerCase(),
|
||||
nickname: w.nickname ? String(w.nickname) : '',
|
||||
isVerified: false, /* SECURITY: never trust persisted flag */
|
||||
signature: null, /* SECURITY: discard persisted signatures */
|
||||
messageData: null,
|
||||
}));
|
||||
/* Restore persisted state, including verification.
|
||||
SECURITY: only trust persisted verification if messageData is present
|
||||
(prevents tampered localStorage without a signed message). */
|
||||
this._wallets = parsed
|
||||
.filter((w) => w && typeof w === 'object' && w.address && w.chain)
|
||||
.map((w) => ({
|
||||
address: String(w.address),
|
||||
chain: String(w.chain).toLowerCase(),
|
||||
nickname: w.nickname ? String(w.nickname) : '',
|
||||
isVerified: !!(w.isVerified && w.messageData),
|
||||
signature: w.signature ? String(w.signature) : null,
|
||||
messageData: w.messageData ? w.messageData : null,
|
||||
}));
|
||||
|
||||
return this._wallets;
|
||||
|
||||
@ -174,6 +174,20 @@ export class WalletManager {
|
||||
return this._wallets.map((w) => ({ ...w }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return wallets that are either verified, or on a non-verification-required chain.
|
||||
* Non-verification chains (btc, solana, etc.) are always included.
|
||||
* EVM-only chains require isVerified === true to be returned.
|
||||
*
|
||||
* @returns {TrackedWallet[]}
|
||||
*/
|
||||
getVerifiedWallets() {
|
||||
return this._wallets.filter((w) => {
|
||||
if (['btc', 'bitcoin', 'solana'].includes(w.chain)) return true;
|
||||
return w.isVerified;
|
||||
}).map((w) => ({ ...w }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new wallet. Rejects duplicates (same address+chain).
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user