feat: Wallet X-Ray — Solana wallet analyser tool (v1.30.0)

- New tool: deep scan any Solana wallet address or connect wallet
- Overview: SOL balance, portfolio value, wallet age, tx count, activity rating
- Token Holdings: SPL token table with logos, prices, USD values via Jupiter
- NFT Detection: 0-decimal single-supply token grid with Solscan links
- Recent Transactions: last 20 sigs with status and timestamps
- Wallet Score: composite A+ to F grading with colour-coded bars
- Token-2022 support, RPC failover, Jupiter token list caching
- URL param support (?address=) for direct scanning
- Radar sweep loading animation, responsive mobile design
- New LAB card with cyan theme
This commit is contained in:
jae 2026-04-18 21:14:13 +00:00
parent b5e7f114ba
commit 660bb842ec
5 changed files with 1746 additions and 0 deletions

View file

@ -1,6 +1,27 @@
{ {
"site": "jaeswift.xyz", "site": "jaeswift.xyz",
"entries": [ "entries": [
{
"version": "v1.30.0",
"date": "18/04/2026",
"title": "ARMOURY: Wallet X-Ray — Solana Wallet Analyser",
"category": "ARMOURY",
"changes": [
"New tool: Wallet X-Ray — deep scan any Solana wallet address or connect your own wallet",
"Overview panel: SOL balance with live USD value, total portfolio value, wallet age, transaction count, and activity rating",
"Token Holdings: full SPL token table with logos, balances, live prices via Jupiter Price API, and USD values sorted by value",
"NFT Detection: identifies NFTs (0-decimal single-supply tokens) with image grid and Solscan links",
"Recent Transactions: last 20 transactions with signatures linked to Solscan, status indicators, and timestamps",
"Wallet Score: composite grading system (A+ to F) based on age, activity, diversity, and portfolio scores with colour-coded progress bars",
"Supports Token-2022 programme accounts alongside standard SPL Token programme",
"Rate-limit resilient: automatic RPC failover to backup endpoint with exponential backoff",
"Jupiter token list cached in sessionStorage for faster repeat scans",
"URL parameter support: ?address=... for direct wallet scanning via shared links",
"Wallet X-Ray card added to LAB page with cyan/turquoise theme",
"Military radar sweep loading animation during wallet scan",
"Fully responsive design — works on mobile and desktop"
]
},
{ {
"version": "v1.29.0", "version": "v1.29.0",
"date": "18/04/2026", "date": "18/04/2026",

View file

@ -49,6 +49,13 @@
border-left-color: #7B61FF; border-left-color: #7B61FF;
box-shadow: 0 4px 20px rgba(123, 97, 255, 0.15); box-shadow: 0 4px 20px rgba(123, 97, 255, 0.15);
} }
.deploy-card--cyan {
border-left-color: rgba(20, 241, 149, 0.6);
}
.deploy-card--cyan:hover {
border-left-color: #14F195;
box-shadow: 0 4px 20px rgba(20, 241, 149, 0.15);
}
.deploy-card-status { .deploy-card-status {
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem; font-size: 0.6rem;
@ -163,6 +170,18 @@
</div> </div>
<div class="deploy-card-url">jaeswift.xyz/jupiterswap</div> <div class="deploy-card-url">jaeswift.xyz/jupiterswap</div>
</a> </a>
<a href="/walletxray/" class="deploy-card deploy-card--cyan">
<div class="deploy-card-status" style="color: #14F195;">◆ EXPERIMENTAL</div>
<div class="deploy-card-title">WALLET X-RAY</div>
<div class="deploy-card-desc">Deep scan any Solana wallet. Token holdings, NFTs, transaction history, PnL analysis, and wallet scoring.</div>
<div class="deploy-card-tags">
<span class="deploy-tag" style="color: rgba(20, 241, 149, 0.8); background: rgba(20, 241, 149, 0.08); border-color: rgba(20, 241, 149, 0.2);">SOLANA</span>
<span class="deploy-tag" style="color: rgba(20, 241, 149, 0.8); background: rgba(20, 241, 149, 0.08); border-color: rgba(20, 241, 149, 0.2);">ANALYSIS</span>
<span class="deploy-tag" style="color: rgba(0, 255, 255, 0.8); background: rgba(0, 255, 255, 0.08); border-color: rgba(0, 255, 255, 0.2);">SCANNER</span>
</div>
<div class="deploy-card-url">jaeswift.xyz/walletxray</div>
</a>
</div> </div>
</section> </section>

741
css/walletxray.css Normal file
View file

@ -0,0 +1,741 @@
/* ===================================================
WALLET X-RAY Solana Wallet Analyser
Military / CRT dark theme #14F195 accent
=================================================== */
/* --- Container --- */
.wx-container {
max-width: clamp(900px, 88vw, 1400px);
margin: 0 auto;
padding: 0 1.5rem 4rem;
}
/* --- Input Bar --- */
.wx-input-bar {
display: flex;
gap: 0.75rem;
align-items: stretch;
background: rgba(16, 16, 16, 0.9);
border: 1px solid var(--border);
padding: 1rem 1.25rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.wx-input-bar input {
flex: 1;
min-width: 200px;
background: rgba(10, 10, 10, 0.8);
border: 1px solid #333;
color: #d8d8d8;
font-family: var(--font-mono);
font-size: 0.75rem;
padding: 0.65rem 1rem;
letter-spacing: 0.5px;
outline: none;
transition: border-color 0.3s;
}
.wx-input-bar input:focus {
border-color: #14F195;
box-shadow: 0 0 8px rgba(20, 241, 149, 0.15);
}
.wx-input-bar input::placeholder {
color: #555;
letter-spacing: 1px;
}
.wx-btn {
font-family: var(--font-display);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 2px;
padding: 0.65rem 1.5rem;
border: 1px solid;
cursor: pointer;
transition: all 0.25s ease;
white-space: nowrap;
}
.wx-btn-scan {
background: rgba(20, 241, 149, 0.1);
border-color: rgba(20, 241, 149, 0.4);
color: #14F195;
}
.wx-btn-scan:hover:not(:disabled) {
background: rgba(20, 241, 149, 0.2);
border-color: #14F195;
box-shadow: 0 0 15px rgba(20, 241, 149, 0.2);
}
.wx-btn-scan:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.wx-btn-wallet {
background: rgba(255, 255, 255, 0.04);
border-color: #444;
color: #999;
}
.wx-btn-wallet:hover {
border-color: #14F195;
color: #14F195;
}
.wx-btn-wallet.connected {
border-color: rgba(20, 241, 149, 0.5);
color: #14F195;
}
.wx-or {
font-family: var(--font-mono);
font-size: 0.6rem;
color: #555;
display: flex;
align-items: center;
letter-spacing: 2px;
}
/* --- Loading Overlay --- */
.wx-loading {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
gap: 1.5rem;
}
.wx-loading.active { display: flex; }
.wx-radar {
width: 120px;
height: 120px;
border-radius: 50%;
border: 2px solid rgba(20, 241, 149, 0.2);
position: relative;
overflow: hidden;
}
.wx-radar::before {
content: '';
position: absolute;
top: 50%; left: 50%;
width: 50%; height: 50%;
transform-origin: 0 0;
background: conic-gradient(from 0deg, transparent 0deg, rgba(20, 241, 149, 0.3) 40deg, transparent 80deg);
animation: wx-sweep 2s linear infinite;
}
.wx-radar::after {
content: '';
position: absolute;
top: 50%; left: 50%;
width: 6px; height: 6px;
background: #14F195;
border-radius: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 0 10px #14F195;
}
.wx-radar-ring {
position: absolute;
border-radius: 50%;
border: 1px solid rgba(20, 241, 149, 0.1);
}
.wx-radar-ring:nth-child(1) { width: 33%; height: 33%; top: 33.5%; left: 33.5%; }
.wx-radar-ring:nth-child(2) { width: 66%; height: 66%; top: 17%; left: 17%; }
@keyframes wx-sweep {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.wx-loading-text {
font-family: var(--font-mono);
font-size: 0.7rem;
color: #14F195;
letter-spacing: 3px;
animation: wx-blink 1s step-end infinite;
}
.wx-loading-sub {
font-family: var(--font-mono);
font-size: 0.6rem;
color: #666;
letter-spacing: 1px;
}
@keyframes wx-blink {
50% { opacity: 0.4; }
}
/* --- Results Container --- */
.wx-results {
display: none;
}
.wx-results.active { display: block; }
/* --- Section Panels --- */
.wx-panel {
background: rgba(16, 16, 16, 0.85);
border: 1px solid var(--border);
margin-bottom: 1.5rem;
position: relative;
overflow: hidden;
}
.wx-panel::before {
content: '';
position: absolute;
top: 0; left: 0;
width: 100%; height: 2px;
background: linear-gradient(90deg, transparent, rgba(20, 241, 149, 0.4), transparent);
}
.wx-panel-header {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--border);
}
.wx-panel-icon {
font-size: 0.9rem;
opacity: 0.7;
}
.wx-panel-label {
font-family: var(--font-display);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 2px;
color: #14F195;
}
.wx-panel-count {
font-family: var(--font-mono);
font-size: 0.55rem;
color: #666;
margin-left: auto;
letter-spacing: 1px;
}
.wx-panel-body {
padding: 1.25rem;
}
/* --- Overview Grid --- */
.wx-overview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
}
.wx-stat-card {
background: rgba(10, 10, 10, 0.6);
border: 1px solid #222;
padding: 1rem;
position: relative;
}
.wx-stat-card::after {
content: '';
position: absolute;
bottom: 0; left: 0;
width: 100%; height: 1px;
background: linear-gradient(90deg, transparent, rgba(20, 241, 149, 0.15), transparent);
}
.wx-stat-label {
font-family: var(--font-mono);
font-size: 0.55rem;
color: #666;
letter-spacing: 2px;
margin-bottom: 0.5rem;
}
.wx-stat-value {
font-family: var(--font-display);
font-size: 1.1rem;
font-weight: 700;
color: #14F195;
letter-spacing: 1px;
}
.wx-stat-value.sol { color: #14F195; }
.wx-stat-value.usd { color: #d0d0d0; }
.wx-stat-sub {
font-family: var(--font-mono);
font-size: 0.6rem;
color: #555;
margin-top: 0.3rem;
}
.wx-activity-badge {
display: inline-block;
font-family: var(--font-mono);
font-size: 0.6rem;
font-weight: 700;
letter-spacing: 2px;
padding: 0.2rem 0.6rem;
border: 1px solid;
}
.wx-activity-badge.active {
color: #14F195;
border-color: rgba(20, 241, 149, 0.4);
background: rgba(20, 241, 149, 0.08);
}
.wx-activity-badge.dormant {
color: #c9a227;
border-color: rgba(201, 162, 39, 0.4);
background: rgba(201, 162, 39, 0.08);
}
.wx-activity-badge.new {
color: #7B61FF;
border-color: rgba(123, 97, 255, 0.4);
background: rgba(123, 97, 255, 0.08);
}
.wx-activity-badge.dead {
color: #ff4444;
border-color: rgba(255, 68, 68, 0.4);
background: rgba(255, 68, 68, 0.08);
}
/* --- Token Table --- */
.wx-token-table {
width: 100%;
border-collapse: collapse;
}
.wx-token-table thead th {
font-family: var(--font-mono);
font-size: 0.55rem;
color: #666;
letter-spacing: 2px;
text-align: left;
padding: 0.6rem 0.75rem;
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
.wx-token-table tbody tr {
border-bottom: 1px solid rgba(42, 42, 42, 0.4);
transition: background 0.2s;
}
.wx-token-table tbody tr:hover {
background: rgba(20, 241, 149, 0.03);
}
.wx-token-table td {
font-family: var(--font-mono);
font-size: 0.7rem;
color: #ccc;
padding: 0.65rem 0.75rem;
vertical-align: middle;
}
.wx-token-row-info {
display: flex;
align-items: center;
gap: 0.6rem;
}
.wx-token-logo {
width: 28px;
height: 28px;
border-radius: 50%;
background: #222;
border: 1px solid #333;
object-fit: cover;
flex-shrink: 0;
}
.wx-token-logo-placeholder {
width: 28px;
height: 28px;
border-radius: 50%;
background: #1a1a1a;
border: 1px solid #333;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.55rem;
color: #555;
flex-shrink: 0;
}
.wx-token-name {
font-size: 0.7rem;
color: #ddd;
font-weight: 500;
}
.wx-token-symbol {
font-size: 0.55rem;
color: #666;
letter-spacing: 1px;
}
.wx-token-value { color: #14F195; }
.wx-change-pos { color: #14F195; }
.wx-change-neg { color: #ff4444; }
.wx-change-zero { color: #555; }
.wx-token-mint {
font-size: 0.55rem;
color: #444;
cursor: pointer;
transition: color 0.2s;
}
.wx-token-mint:hover { color: #14F195; }
/* --- NFT Grid --- */
.wx-nft-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 1rem;
}
.wx-nft-card {
background: rgba(10, 10, 10, 0.6);
border: 1px solid #222;
overflow: hidden;
transition: all 0.25s;
}
.wx-nft-card:hover {
border-color: rgba(20, 241, 149, 0.4);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(20, 241, 149, 0.1);
}
.wx-nft-img {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
background: #111;
display: block;
}
.wx-nft-img-placeholder {
width: 100%;
aspect-ratio: 1;
background: #111;
display: flex;
align-items: center;
justify-content: center;
color: #333;
font-size: 1.5rem;
}
.wx-nft-info {
padding: 0.6rem;
}
.wx-nft-name {
font-family: var(--font-mono);
font-size: 0.6rem;
color: #ccc;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.wx-nft-collection {
font-family: var(--font-mono);
font-size: 0.5rem;
color: #555;
letter-spacing: 1px;
margin-top: 0.2rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* --- Transaction List --- */
.wx-tx-list {
display: flex;
flex-direction: column;
}
.wx-tx-row {
display: grid;
grid-template-columns: 120px 1fr 100px 140px;
gap: 1rem;
padding: 0.6rem 0.75rem;
border-bottom: 1px solid rgba(42, 42, 42, 0.3);
align-items: center;
transition: background 0.2s;
}
.wx-tx-row:hover { background: rgba(20, 241, 149, 0.03); }
.wx-tx-sig {
font-family: var(--font-mono);
font-size: 0.65rem;
color: var(--link-color);
text-decoration: none;
letter-spacing: 0.5px;
}
.wx-tx-sig:hover { color: #14F195; }
.wx-tx-type {
font-family: var(--font-mono);
font-size: 0.6rem;
letter-spacing: 1px;
}
.wx-tx-type.transfer { color: #14F195; }
.wx-tx-type.swap { color: #7B61FF; }
.wx-tx-type.mint { color: #F5A623; }
.wx-tx-type.unknown { color: #666; }
.wx-tx-amount {
font-family: var(--font-mono);
font-size: 0.65rem;
color: #ccc;
text-align: right;
}
.wx-tx-time {
font-family: var(--font-mono);
font-size: 0.6rem;
color: #555;
text-align: right;
}
.wx-tx-header {
display: grid;
grid-template-columns: 120px 1fr 100px 140px;
gap: 1rem;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--border);
}
.wx-tx-header span {
font-family: var(--font-mono);
font-size: 0.5rem;
color: #555;
letter-spacing: 2px;
}
.wx-tx-header span:nth-child(3),
.wx-tx-header span:nth-child(4) { text-align: right; }
/* --- Wallet Score --- */
.wx-score-container {
display: grid;
grid-template-columns: 200px 1fr;
gap: 2rem;
align-items: center;
}
.wx-grade-circle {
width: 160px;
height: 160px;
border-radius: 50%;
border: 3px solid;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0 auto;
position: relative;
}
.wx-grade-circle::before {
content: '';
position: absolute;
inset: -8px;
border-radius: 50%;
border: 1px solid;
opacity: 0.2;
}
.wx-grade-letter {
font-family: var(--font-display);
font-size: 2.5rem;
font-weight: 900;
letter-spacing: 2px;
}
.wx-grade-label {
font-family: var(--font-mono);
font-size: 0.55rem;
letter-spacing: 2px;
opacity: 0.6;
margin-top: 0.2rem;
}
.wx-grade-a { color: #14F195; border-color: rgba(20, 241, 149, 0.5); }
.wx-grade-a::before { border-color: #14F195; }
.wx-grade-b { color: #00cc33; border-color: rgba(0, 204, 51, 0.5); }
.wx-grade-b::before { border-color: #00cc33; }
.wx-grade-c { color: #c9a227; border-color: rgba(201, 162, 39, 0.5); }
.wx-grade-c::before { border-color: #c9a227; }
.wx-grade-d { color: #F5A623; border-color: rgba(245, 166, 35, 0.5); }
.wx-grade-d::before { border-color: #F5A623; }
.wx-grade-f { color: #ff4444; border-color: rgba(255, 68, 68, 0.5); }
.wx-grade-f::before { border-color: #ff4444; }
.wx-score-bars {
display: flex;
flex-direction: column;
gap: 1rem;
}
.wx-score-item {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.wx-score-item-header {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.wx-score-item-label {
font-family: var(--font-mono);
font-size: 0.6rem;
color: #999;
letter-spacing: 2px;
}
.wx-score-item-val {
font-family: var(--font-display);
font-size: 0.7rem;
font-weight: 700;
color: #14F195;
}
.wx-score-bar-track {
height: 6px;
background: #1a1a1a;
border: 1px solid #222;
position: relative;
overflow: hidden;
}
.wx-score-bar-fill {
height: 100%;
transition: width 0.8s ease;
position: relative;
}
.wx-score-bar-fill::after {
content: '';
position: absolute;
right: 0; top: 0;
width: 2px; height: 100%;
background: inherit;
box-shadow: 0 0 8px currentColor;
}
.wx-score-bar-fill.green { background: #14F195; color: #14F195; }
.wx-score-bar-fill.yellow { background: #c9a227; color: #c9a227; }
.wx-score-bar-fill.orange { background: #F5A623; color: #F5A623; }
.wx-score-bar-fill.red { background: #ff4444; color: #ff4444; }
/* --- Empty / Error States --- */
.wx-empty {
text-align: center;
padding: 2rem;
font-family: var(--font-mono);
font-size: 0.7rem;
color: #555;
letter-spacing: 1px;
}
.wx-error-msg {
background: rgba(255, 68, 68, 0.05);
border: 1px solid rgba(255, 68, 68, 0.2);
color: #ff4444;
font-family: var(--font-mono);
font-size: 0.7rem;
padding: 1rem 1.25rem;
margin-bottom: 1.5rem;
display: none;
letter-spacing: 0.5px;
}
.wx-error-msg.active { display: block; }
/* --- Section Loading Spinners --- */
.wx-section-loading {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 0;
}
.wx-mini-spinner {
width: 16px;
height: 16px;
border: 2px solid #222;
border-top-color: #14F195;
border-radius: 50%;
animation: wx-spin 0.8s linear infinite;
}
@keyframes wx-spin {
to { transform: rotate(360deg); }
}
.wx-section-loading span {
font-family: var(--font-mono);
font-size: 0.6rem;
color: #555;
letter-spacing: 1px;
}
/* --- Responsive --- */
@media (max-width: 768px) {
.wx-input-bar { flex-direction: column; }
.wx-or { justify-content: center; padding: 0.25rem 0; }
.wx-overview-grid { grid-template-columns: repeat(2, 1fr); }
.wx-score-container {
grid-template-columns: 1fr;
text-align: center;
}
.wx-tx-row {
grid-template-columns: 1fr 1fr;
gap: 0.4rem;
}
.wx-tx-header { display: none; }
.wx-tx-amount, .wx-tx-time { text-align: left; }
.wx-nft-grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); }
.wx-token-table { font-size: 0.6rem; }
.wx-token-table .wx-col-change,
.wx-token-table .wx-col-mint { display: none; }
}
@media (max-width: 480px) {
.wx-overview-grid { grid-template-columns: 1fr; }
.wx-stat-value { font-size: 0.9rem; }
.wx-grade-circle { width: 120px; height: 120px; }
.wx-grade-letter { font-size: 2rem; }
}

796
js/walletxray.js Normal file
View file

@ -0,0 +1,796 @@
/* Wallet X-Ray Solana Wallet Analyser
Full client-side scan: SOL balance, SPL tokens, NFTs,
transactions, and wallet scoring.
*/
(function () {
'use strict';
// ── Config ──
const RPC = 'https://api.mainnet-beta.solana.com';
const RPC_BACKUP = 'https://rpc.ankr.com/solana';
const JUP_PRICE = 'https://price.jup.ag/v6/price';
const JUP_TOKEN_LIST = 'https://token.jup.ag/all';
const SOLSCAN_TX = 'https://solscan.io/tx/';
const SOLSCAN_ACCT = 'https://solscan.io/account/';
const TOKEN_PROGRAM = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
const TOKEN_2022 = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
const SOL_MINT = 'So11111111111111111111111111111111111111112';
const LAMPORTS = 1e9;
// ── DOM refs ──
const $addr = document.getElementById('wx-address');
const $scanBtn = document.getElementById('wx-scan-btn');
const $walBtn = document.getElementById('wx-wallet-btn');
const $error = document.getElementById('wx-error');
const $loading = document.getElementById('wx-loading');
const $loadTxt = document.getElementById('wx-loading-text');
const $loadSub = document.getElementById('wx-loading-sub');
const $results = document.getElementById('wx-results');
// ── State ──
let tokenListCache = null;
let tokenMapCache = null; // mint → {name, symbol, logoURI, decimals}
let activeRpc = RPC;
let scanning = false;
// ── Helpers ──
function truncAddr(a) {
if (!a || a.length < 12) return a || '';
return a.slice(0, 4) + '...' + a.slice(-4);
}
function fmtUsd(n) {
if (n == null || isNaN(n)) return '$—';
if (n >= 1e6) return '$' + (n / 1e6).toFixed(2) + 'M';
if (n >= 1e3) return '$' + (n / 1e3).toFixed(2) + 'K';
return '$' + n.toFixed(2);
}
function fmtSol(n) {
if (n == null || isNaN(n)) return '—';
return n.toFixed(4) + ' SOL';
}
function fmtNum(n) {
if (n == null || isNaN(n)) return '—';
return n.toLocaleString('en-US', { maximumFractionDigits: 4 });
}
function fmtDate(ts) {
if (!ts) return '—';
const d = new Date(ts * 1000);
return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
}
function fmtTime(ts) {
if (!ts) return '—';
const d = new Date(ts * 1000);
return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' }) + ' ' +
d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
}
function daysBetween(ts1, ts2) {
return Math.floor(Math.abs(ts2 - ts1) / 86400);
}
function isValidSolAddress(addr) {
return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(addr);
}
function showError(msg) {
$error.textContent = '✖ ' + msg;
$error.classList.add('active');
}
function hideError() {
$error.classList.remove('active');
$error.textContent = '';
}
function setLoading(show, text, sub) {
if (show) {
$loading.classList.add('active');
$results.classList.remove('active');
if (text) $loadTxt.textContent = text;
if (sub) $loadSub.textContent = sub;
} else {
$loading.classList.remove('active');
}
}
function sectionLoading(id, show) {
const el = document.getElementById(id);
if (el) el.style.display = show ? 'flex' : 'none';
}
// ── RPC Call ──
async function rpc(method, params, retries = 2) {
for (let i = 0; i <= retries; i++) {
try {
const url = i === 0 ? activeRpc : RPC_BACKUP;
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
});
if (res.status === 429) {
await sleep(1000 * (i + 1));
continue;
}
const json = await res.json();
if (json.error) {
if (json.error.code === -32429 || json.error.message?.includes('rate')) {
await sleep(1000 * (i + 1));
continue;
}
throw new Error(json.error.message || 'RPC error');
}
return json.result;
} catch (e) {
if (i === retries) throw e;
activeRpc = RPC_BACKUP;
await sleep(500);
}
}
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
// ── Token List ──
async function loadTokenList() {
if (tokenMapCache) return tokenMapCache;
// Try sessionStorage first
try {
const cached = sessionStorage.getItem('wx_token_list');
if (cached) {
const parsed = JSON.parse(cached);
if (parsed._ts && Date.now() - parsed._ts < 3600000) {
tokenListCache = parsed.list;
tokenMapCache = buildTokenMap(tokenListCache);
return tokenMapCache;
}
}
} catch (e) { /* ignore */ }
try {
const res = await fetch(JUP_TOKEN_LIST);
tokenListCache = await res.json();
tokenMapCache = buildTokenMap(tokenListCache);
try {
sessionStorage.setItem('wx_token_list', JSON.stringify({
_ts: Date.now(),
list: tokenListCache.slice(0, 5000) // Store top tokens only
}));
} catch (e) { /* storage full */ }
return tokenMapCache;
} catch (e) {
console.warn('[WX] Failed to load token list:', e);
tokenMapCache = new Map();
return tokenMapCache;
}
}
function buildTokenMap(list) {
const map = new Map();
if (!Array.isArray(list)) return map;
for (const t of list) {
if (t.address) {
map.set(t.address, {
name: t.name || 'Unknown',
symbol: t.symbol || '???',
logoURI: t.logoURI || null,
decimals: t.decimals ?? 0
});
}
}
return map;
}
// ── Jupiter Prices ──
async function fetchPrices(mints) {
if (!mints || mints.length === 0) return {};
const prices = {};
// Batch in groups of 100
const batches = [];
for (let i = 0; i < mints.length; i += 100) {
batches.push(mints.slice(i, i + 100));
}
for (const batch of batches) {
try {
const ids = batch.join(',');
const res = await fetch(JUP_PRICE + '?ids=' + ids);
const json = await res.json();
if (json.data) {
for (const [mint, info] of Object.entries(json.data)) {
prices[mint] = info.price || 0;
}
}
} catch (e) {
console.warn('[WX] Price fetch error:', e);
}
if (batches.length > 1) await sleep(300);
}
return prices;
}
// ══════════════════════════════════════════════════════════
// MAIN SCAN
// ══════════════════════════════════════════════════════════
async function scan(address) {
if (scanning) return;
if (!isValidSolAddress(address)) {
showError('Invalid Solana address. Check the format and try again.');
return;
}
scanning = true;
hideError();
setLoading(true, 'SCANNING WALLET', 'Querying Solana mainnet...');
$scanBtn.disabled = true;
// Reset result sections
document.getElementById('wx-overview').innerHTML = '';
document.getElementById('wx-tokens-content').innerHTML = '';
document.getElementById('wx-nfts-content').innerHTML = '';
document.getElementById('wx-tx-content').innerHTML = '';
document.getElementById('wx-score-content').innerHTML = '';
document.getElementById('wx-wallet-short').textContent = truncAddr(address);
document.getElementById('wx-token-count').textContent = '';
document.getElementById('wx-nft-count').textContent = '';
document.getElementById('wx-tx-count').textContent = '';
try {
// Load token list in background
const tokenMapPromise = loadTokenList();
// ── Step 1: SOL Balance ──
setLoading(true, 'READING SOL BALANCE', 'getBalance...');
const balResult = await rpc('getBalance', [address, { commitment: 'confirmed' }]);
const solBalance = (balResult?.value || 0) / LAMPORTS;
// ── Step 2: Token Accounts ──
setLoading(true, 'SCANNING TOKEN ACCOUNTS', 'getTokenAccountsByOwner...');
const tokenAccounts = await rpc('getTokenAccountsByOwner', [
address,
{ programId: TOKEN_PROGRAM },
{ encoding: 'jsonParsed' }
]);
// Also check Token-2022
let token2022Accounts = null;
try {
token2022Accounts = await rpc('getTokenAccountsByOwner', [
address,
{ programId: TOKEN_2022 },
{ encoding: 'jsonParsed' }
]);
} catch (e) { /* ignore */ }
const allAccounts = [
...(tokenAccounts?.value || []),
...(token2022Accounts?.value || [])
];
// Parse token accounts
const tokens = [];
const nftCandidates = [];
for (const acct of allAccounts) {
const info = acct.account?.data?.parsed?.info;
if (!info) continue;
const mint = info.mint;
const amount = parseFloat(info.tokenAmount?.uiAmountString || '0');
const decimals = info.tokenAmount?.decimals || 0;
if (amount === 0) continue; // Skip zero balances
if (decimals === 0 && amount === 1) {
nftCandidates.push({ mint, amount });
} else {
tokens.push({ mint, amount, decimals });
}
}
// ── Step 3: Fetch Prices ──
setLoading(true, 'FETCHING PRICES', 'Querying Jupiter Price API...');
const tokenMap = await tokenMapPromise;
const priceMints = [SOL_MINT, ...tokens.map(t => t.mint)];
const prices = await fetchPrices(priceMints);
const solPrice = prices[SOL_MINT] || 0;
// Enrich tokens with metadata and prices
const enrichedTokens = tokens.map(t => {
const meta = tokenMap.get(t.mint) || { name: 'Unknown Token', symbol: '???', logoURI: null };
const price = prices[t.mint] || 0;
const usdValue = t.amount * price;
return { ...t, ...meta, price, usdValue };
}).sort((a, b) => b.usdValue - a.usdValue);
// ── Step 4: Transactions ──
setLoading(true, 'LOADING TRANSACTIONS', 'getSignaturesForAddress...');
let signatures = [];
try {
signatures = await rpc('getSignaturesForAddress', [
address,
{ limit: 20, commitment: 'confirmed' }
]) || [];
} catch (e) {
console.warn('[WX] Failed to fetch signatures:', e);
}
// ── Step 5: Calculate wallet age ──
let oldestTx = null;
let walletAge = 0;
let totalTxCount = 0;
try {
// Get oldest signature for age
const oldestSigs = await rpc('getSignaturesForAddress', [
address,
{ limit: 1, commitment: 'confirmed', before: undefined }
]);
// Try to get a rough total count — fetch the last page
// We'll use the first tx time and recent tx to estimate
if (signatures.length > 0) {
const newest = signatures[0]?.blockTime || 0;
// Try to find oldest
let lastSig = signatures[signatures.length - 1]?.signature;
let oldest = signatures[signatures.length - 1]?.blockTime || 0;
let fetchMore = true;
let pages = 0;
totalTxCount = signatures.length;
while (fetchMore && pages < 50) {
try {
const more = await rpc('getSignaturesForAddress', [
address,
{ limit: 1000, before: lastSig, commitment: 'confirmed' }
]);
if (!more || more.length === 0) {
fetchMore = false;
} else {
totalTxCount += more.length;
lastSig = more[more.length - 1].signature;
oldest = more[more.length - 1].blockTime || oldest;
if (more.length < 1000) fetchMore = false;
}
pages++;
await sleep(200);
} catch (e) {
fetchMore = false;
}
}
oldestTx = oldest;
if (oldest) {
walletAge = daysBetween(oldest, Date.now() / 1000);
}
}
} catch (e) {
console.warn('[WX] Error getting wallet age:', e);
if (signatures.length > 0) {
oldestTx = signatures[signatures.length - 1]?.blockTime;
totalTxCount = signatures.length;
if (oldestTx) walletAge = daysBetween(oldestTx, Date.now() / 1000);
}
}
// ── RENDER EVERYTHING ──
setLoading(false);
$results.classList.add('active');
// Calculate portfolio total
const solUsd = solBalance * solPrice;
const tokenUsd = enrichedTokens.reduce((sum, t) => sum + t.usdValue, 0);
const totalPortfolio = solUsd + tokenUsd;
// Activity rating
let activityRating = 'DORMANT';
let activityClass = 'dormant';
if (walletAge < 30) { activityRating = 'NEW'; activityClass = 'new'; }
else if (totalTxCount > 500 && walletAge > 0 && (totalTxCount / walletAge) > 0.5) { activityRating = 'ACTIVE'; activityClass = 'active'; }
else if (totalTxCount > 100) { activityRating = 'MODERATE'; activityClass = 'active'; }
else if (totalTxCount < 10 && walletAge > 180) { activityRating = 'DEAD'; activityClass = 'dead'; }
// ── Render Overview ──
renderOverview({
solBalance, solPrice, solUsd, totalPortfolio,
walletAge, oldestTx, totalTxCount, activityRating, activityClass,
tokenCount: enrichedTokens.length, nftCount: nftCandidates.length
});
// ── Render Tokens ──
sectionLoading('wx-tokens-loading', false);
renderTokens(enrichedTokens);
// ── Render NFTs ──
sectionLoading('wx-nfts-loading', false);
renderNfts(nftCandidates, tokenMap);
// ── Render Transactions ──
sectionLoading('wx-tx-loading', false);
renderTransactions(signatures, address);
// ── Render Wallet Score ──
sectionLoading('wx-score-loading', false);
renderScore({
walletAge, totalTxCount,
tokenCount: enrichedTokens.length,
nftCount: nftCandidates.length,
solBalance, totalPortfolio
});
} catch (err) {
setLoading(false);
showError('Scan failed: ' + (err.message || 'Unknown error. The RPC may be rate-limited — try again in a few seconds.'));
console.error('[WX] Scan error:', err);
} finally {
scanning = false;
$scanBtn.disabled = false;
}
}
// ══════════════════════════════════════════════════════════
// RENDERERS
// ══════════════════════════════════════════════════════════
function renderOverview(d) {
const container = document.getElementById('wx-overview');
container.innerHTML = `
<div class="wx-stat-card">
<div class="wx-stat-label">SOL BALANCE</div>
<div class="wx-stat-value sol">${fmtSol(d.solBalance)}</div>
<div class="wx-stat-sub">${fmtUsd(d.solUsd)} @ ${fmtUsd(d.solPrice)}/SOL</div>
</div>
<div class="wx-stat-card">
<div class="wx-stat-label">PORTFOLIO VALUE</div>
<div class="wx-stat-value usd">${fmtUsd(d.totalPortfolio)}</div>
<div class="wx-stat-sub">${d.tokenCount} tokens + ${d.nftCount} NFTs</div>
</div>
<div class="wx-stat-card">
<div class="wx-stat-label">WALLET AGE</div>
<div class="wx-stat-value">${d.walletAge > 0 ? d.walletAge + 'd' : '—'}</div>
<div class="wx-stat-sub">${d.oldestTx ? 'Since ' + fmtDate(d.oldestTx) : 'No transactions found'}</div>
</div>
<div class="wx-stat-card">
<div class="wx-stat-label">TOTAL TRANSACTIONS</div>
<div class="wx-stat-value">${d.totalTxCount.toLocaleString()}</div>
<div class="wx-stat-sub">${d.walletAge > 0 ? (d.totalTxCount / d.walletAge).toFixed(1) + ' tx/day avg' : '—'}</div>
</div>
<div class="wx-stat-card">
<div class="wx-stat-label">ACTIVITY RATING</div>
<div class="wx-stat-value"><span class="wx-activity-badge ${d.activityClass}">${d.activityRating}</span></div>
</div>
`;
}
function renderTokens(tokens) {
const container = document.getElementById('wx-tokens-content');
const countEl = document.getElementById('wx-token-count');
countEl.textContent = tokens.length + ' TOKEN' + (tokens.length !== 1 ? 'S' : '');
if (tokens.length === 0) {
container.innerHTML = '<div class="wx-empty">No SPL tokens found in this wallet.</div>';
return;
}
let html = `
<div style="overflow-x:auto">
<table class="wx-token-table">
<thead>
<tr>
<th>TOKEN</th>
<th>BALANCE</th>
<th>PRICE</th>
<th>VALUE</th>
<th class="wx-col-mint">MINT</th>
</tr>
</thead>
<tbody>
`;
for (const t of tokens) {
const logo = t.logoURI
? `<img class="wx-token-logo" src="${t.logoURI}" alt="${t.symbol}" loading="lazy" onerror="this.outerHTML='<div class=wx-token-logo-placeholder>?</div>'">`
: '<div class="wx-token-logo-placeholder">?</div>';
html += `
<tr>
<td>
<div class="wx-token-row-info">
${logo}
<div>
<div class="wx-token-name">${escHtml(t.name)}</div>
<div class="wx-token-symbol">${escHtml(t.symbol)}</div>
</div>
</div>
</td>
<td>${fmtNum(t.amount)}</td>
<td>${t.price > 0 ? fmtUsd(t.price) : '—'}</td>
<td class="wx-token-value">${t.usdValue > 0 ? fmtUsd(t.usdValue) : '—'}</td>
<td class="wx-col-mint">
<a href="${SOLSCAN_ACCT}${t.mint}" target="_blank" rel="noopener" class="wx-token-mint" title="${t.mint}">${truncAddr(t.mint)}</a>
</td>
</tr>
`;
}
html += '</tbody></table></div>';
container.innerHTML = html;
}
function renderNfts(nfts, tokenMap) {
const container = document.getElementById('wx-nfts-content');
const countEl = document.getElementById('wx-nft-count');
countEl.textContent = nfts.length + ' NFT' + (nfts.length !== 1 ? 'S' : '');
if (nfts.length === 0) {
container.innerHTML = '<div class="wx-empty">No NFTs detected in this wallet.</div>';
return;
}
let html = '<div class="wx-nft-grid">';
for (const nft of nfts) {
const meta = tokenMap.get(nft.mint) || { name: truncAddr(nft.mint), symbol: 'NFT', logoURI: null };
const img = meta.logoURI
? `<img class="wx-nft-img" src="${meta.logoURI}" alt="${escHtml(meta.name)}" loading="lazy" onerror="this.outerHTML='<div class=wx-nft-img-placeholder>🖼</div>'">`
: '<div class="wx-nft-img-placeholder">🖼</div>';
html += `
<a href="${SOLSCAN_ACCT}${nft.mint}" target="_blank" rel="noopener" class="wx-nft-card">
${img}
<div class="wx-nft-info">
<div class="wx-nft-name">${escHtml(meta.name)}</div>
<div class="wx-nft-collection">${escHtml(meta.symbol)}</div>
</div>
</a>
`;
}
html += '</div>';
container.innerHTML = html;
}
function renderTransactions(sigs, address) {
const container = document.getElementById('wx-tx-content');
const countEl = document.getElementById('wx-tx-count');
countEl.textContent = 'LAST ' + sigs.length;
if (sigs.length === 0) {
container.innerHTML = '<div class="wx-empty">No recent transactions found.</div>';
return;
}
let html = `
<div class="wx-tx-header">
<span>SIGNATURE</span>
<span>TYPE</span>
<span>STATUS</span>
<span>TIME</span>
</div>
<div class="wx-tx-list">
`;
for (const sig of sigs) {
const signature = sig.signature || '';
const truncSig = signature.slice(0, 8) + '...' + signature.slice(-4);
const time = fmtTime(sig.blockTime);
const err = sig.err;
const status = err ? 'FAILED' : 'SUCCESS';
const statusClass = err ? 'wx-change-neg' : 'wx-change-pos';
// Classify based on memo and other signals
let type = 'UNKNOWN';
let typeClass = 'unknown';
if (sig.memo) {
type = 'MEMO';
typeClass = 'transfer';
}
// We'll use a basic heuristic — without fetching full tx details
// (to avoid rate limiting), classify by slot patterns
if (!err) {
type = 'TRANSACTION';
typeClass = 'transfer';
}
html += `
<div class="wx-tx-row">
<a href="${SOLSCAN_TX}${signature}" target="_blank" rel="noopener" class="wx-tx-sig">${truncSig}</a>
<span class="wx-tx-type ${typeClass}">${type}</span>
<span class="wx-tx-amount ${statusClass}">${status}</span>
<span class="wx-tx-time">${time}</span>
</div>
`;
}
html += '</div>';
container.innerHTML = html;
}
function renderScore(d) {
const container = document.getElementById('wx-score-content');
// Calculate individual scores (0100)
const ageScore = Math.min(100, Math.floor((d.walletAge / 730) * 100)); // 2 years = 100
const activityScore = Math.min(100, Math.floor((d.totalTxCount / 1000) * 100)); // 1000 tx = 100
const diversityScore = Math.min(100, Math.floor(((d.tokenCount + d.nftCount) / 50) * 100)); // 50 assets = 100
const wealthScore = Math.min(100, Math.floor((d.totalPortfolio / 10000) * 100)); // $10k = 100
// Overall
const overall = Math.round((ageScore * 0.25) + (activityScore * 0.30) + (diversityScore * 0.20) + (wealthScore * 0.25));
// Grade
let grade, gradeClass;
if (overall >= 85) { grade = 'A+'; gradeClass = 'wx-grade-a'; }
else if (overall >= 70) { grade = 'A'; gradeClass = 'wx-grade-a'; }
else if (overall >= 55) { grade = 'B'; gradeClass = 'wx-grade-b'; }
else if (overall >= 40) { grade = 'C'; gradeClass = 'wx-grade-c'; }
else if (overall >= 25) { grade = 'D'; gradeClass = 'wx-grade-d'; }
else { grade = 'F'; gradeClass = 'wx-grade-f'; }
function barColor(val) {
if (val >= 70) return 'green';
if (val >= 45) return 'yellow';
if (val >= 25) return 'orange';
return 'red';
}
container.innerHTML = `
<div class="wx-score-container">
<div>
<div class="wx-grade-circle ${gradeClass}">
<div class="wx-grade-letter">${grade}</div>
<div class="wx-grade-label">OVERALL ${overall}/100</div>
</div>
</div>
<div class="wx-score-bars">
<div class="wx-score-item">
<div class="wx-score-item-header">
<span class="wx-score-item-label">AGE SCORE</span>
<span class="wx-score-item-val">${ageScore}/100</span>
</div>
<div class="wx-score-bar-track">
<div class="wx-score-bar-fill ${barColor(ageScore)}" style="width:${ageScore}%"></div>
</div>
</div>
<div class="wx-score-item">
<div class="wx-score-item-header">
<span class="wx-score-item-label">ACTIVITY SCORE</span>
<span class="wx-score-item-val">${activityScore}/100</span>
</div>
<div class="wx-score-bar-track">
<div class="wx-score-bar-fill ${barColor(activityScore)}" style="width:${activityScore}%"></div>
</div>
</div>
<div class="wx-score-item">
<div class="wx-score-item-header">
<span class="wx-score-item-label">DIVERSITY SCORE</span>
<span class="wx-score-item-val">${diversityScore}/100</span>
</div>
<div class="wx-score-bar-track">
<div class="wx-score-bar-fill ${barColor(diversityScore)}" style="width:${diversityScore}%"></div>
</div>
</div>
<div class="wx-score-item">
<div class="wx-score-item-header">
<span class="wx-score-item-label">PORTFOLIO SCORE</span>
<span class="wx-score-item-val">${wealthScore}/100</span>
</div>
<div class="wx-score-bar-track">
<div class="wx-score-bar-fill ${barColor(wealthScore)}" style="width:${wealthScore}%"></div>
</div>
</div>
</div>
</div>
`;
}
// ── HTML escape ──
function escHtml(str) {
if (!str) return '';
const el = document.createElement('span');
el.textContent = str;
return el.innerHTML;
}
// ══════════════════════════════════════════════════════════
// EVENT HANDLERS
// ═════════════════════════════════<E29590><E29590><EFBFBD>════════════════════════
// Scan button
$scanBtn.addEventListener('click', () => {
const addr = $addr.value.trim();
if (addr) scan(addr);
else showError('Please enter a Solana wallet address or connect your wallet.');
});
// Enter key
$addr.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
$scanBtn.click();
}
});
// Connect wallet button
$walBtn.addEventListener('click', async () => {
if (!window.solWallet) {
showError('No wallet detected. Install Phantom, Solflare, or another Solana wallet.');
return;
}
if (window.solWallet.connected && window.solWallet.address) {
// Already connected — use address
$addr.value = window.solWallet.address;
scan(window.solWallet.address);
return;
}
// Show wallet picker
const available = window.solWallet.getAvailableWallets();
if (available.length === 0) {
showError('No Solana wallet extensions detected. Install Phantom or Solflare.');
return;
}
// If only one wallet, connect directly
if (available.length === 1) {
try {
const result = await window.solWallet.connect(available[0]);
$addr.value = result.address;
$walBtn.textContent = window.solWallet.truncAddr(result.address);
$walBtn.classList.add('connected');
scan(result.address);
} catch (err) {
showError('Wallet connection failed: ' + (err.message || 'User rejected'));
}
return;
}
// Multiple wallets — connect first available (nav handles picker)
try {
const result = await window.solWallet.connect(available[0]);
$addr.value = result.address;
$walBtn.textContent = window.solWallet.truncAddr(result.address);
$walBtn.classList.add('connected');
scan(result.address);
} catch (err) {
showError('Wallet connection failed: ' + (err.message || 'User rejected'));
}
});
// Listen for wallet connection from nav
window.addEventListener('wallet-connected', (e) => {
const addr = e.detail?.address;
if (addr) {
$addr.value = addr;
$walBtn.textContent = truncAddr(addr);
$walBtn.classList.add('connected');
}
});
window.addEventListener('wallet-disconnected', () => {
$walBtn.textContent = 'CONNECT WALLET';
$walBtn.classList.remove('connected');
});
// Auto-populate if wallet already connected
if (window.solWallet?.connected && window.solWallet.address) {
$addr.value = window.solWallet.address;
$walBtn.textContent = truncAddr(window.solWallet.address);
$walBtn.classList.add('connected');
}
// Check URL params for address
const urlParams = new URLSearchParams(window.location.search);
const paramAddr = urlParams.get('address') || urlParams.get('wallet');
if (paramAddr && isValidSolAddress(paramAddr)) {
$addr.value = paramAddr;
// Auto-scan after slight delay
setTimeout(() => scan(paramAddr), 500);
}
})();

169
walletxray/index.html Normal file
View file

@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JAESWIFT // WALLET X-RAY</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/section.css">
<link rel="stylesheet" href="/css/walletxray.css">
</head>
<body>
<div class="scanline-overlay"></div>
<div class="grid-bg"></div>
<nav class="nav-main" id="navbar">
<div class="nav-container">
<a href="/" class="nav-logo">
<span class="logo-bracket">[</span> JAE <span class="logo-bracket">]</span>
</a>
<button class="nav-toggle" id="navToggle" aria-label="Menu">
<span></span><span></span><span></span>
</button>
<ul class="nav-menu" id="navMenu"></ul>
<div class="nav-status">
<span class="nav-clock" id="navClock"></span>
</div>
</div>
</nav>
<div class="breadcrumb">
<a href="/">HOME</a>
<span class="separator">/</span>
<a href="/armoury">ARMOURY</a>
<span class="separator">/</span>
<a href="/armoury/lab.html">LAB</a>
<span class="separator">/</span>
<span class="current">WALLET X-RAY</span>
</div>
<section class="section-header" style="padding-top: calc(var(--nav-height) + 1.5rem);">
<div class="section-header-label">LAB // SOLANA WALLET ANALYSIS</div>
<h1 class="section-header-title">WALLET X-RAY</h1>
<p class="section-header-sub">&gt; Deep scan any Solana wallet &mdash; token holdings, NFTs, transactions, and wallet scoring.</p>
</section>
<div class="wx-container">
<!-- Input Bar -->
<div class="wx-input-bar">
<input type="text" id="wx-address" placeholder="Paste any Solana wallet address..." spellcheck="false" autocomplete="off">
<button class="wx-btn wx-btn-scan" id="wx-scan-btn">&#9660; SCAN</button>
<span class="wx-or">OR</span>
<button class="wx-btn wx-btn-wallet" id="wx-wallet-btn">CONNECT WALLET</button>
</div>
<!-- Error Display -->
<div class="wx-error-msg" id="wx-error"></div>
<!-- Loading Animation -->
<div class="wx-loading" id="wx-loading">
<div class="wx-radar">
<div class="wx-radar-ring"></div>
<div class="wx-radar-ring"></div>
</div>
<div class="wx-loading-text" id="wx-loading-text">SCANNING WALLET</div>
<div class="wx-loading-sub" id="wx-loading-sub">Querying Solana mainnet...</div>
</div>
<!-- Results -->
<div class="wx-results" id="wx-results">
<!-- SECTION 1: OVERVIEW -->
<div class="wx-panel">
<div class="wx-panel-header">
<span class="wx-panel-icon">&#9632;</span>
<span class="wx-panel-label">OVERVIEW</span>
<span class="wx-panel-count" id="wx-wallet-short"></span>
</div>
<div class="wx-panel-body">
<div class="wx-overview-grid" id="wx-overview"></div>
</div>
</div>
<!-- SECTION 2: TOKEN HOLDINGS -->
<div class="wx-panel">
<div class="wx-panel-header">
<span class="wx-panel-icon">&#9670;</span>
<span class="wx-panel-label">TOKEN HOLDINGS</span>
<span class="wx-panel-count" id="wx-token-count"></span>
</div>
<div class="wx-panel-body" id="wx-tokens-body">
<div class="wx-section-loading" id="wx-tokens-loading">
<div class="wx-mini-spinner"></div>
<span>Fetching token accounts...</span>
</div>
<div id="wx-tokens-content"></div>
</div>
</div>
<!-- SECTION 3: NFTs -->
<div class="wx-panel">
<div class="wx-panel-header">
<span class="wx-panel-icon">&#9827;</span>
<span class="wx-panel-label">NFTs</span>
<span class="wx-panel-count" id="wx-nft-count"></span>
</div>
<div class="wx-panel-body" id="wx-nfts-body">
<div class="wx-section-loading" id="wx-nfts-loading">
<div class="wx-mini-spinner"></div>
<span>Scanning NFT holdings...</span>
</div>
<div id="wx-nfts-content"></div>
</div>
</div>
<!-- SECTION 4: RECENT TRANSACTIONS -->
<div class="wx-panel">
<div class="wx-panel-header">
<span class="wx-panel-icon">&#9654;</span>
<span class="wx-panel-label">RECENT TRANSACTIONS</span>
<span class="wx-panel-count" id="wx-tx-count"></span>
</div>
<div class="wx-panel-body" id="wx-tx-body">
<div class="wx-section-loading" id="wx-tx-loading">
<div class="wx-mini-spinner"></div>
<span>Loading transaction history...</span>
</div>
<div id="wx-tx-content"></div>
</div>
</div>
<!-- SECTION 5: WALLET SCORE -->
<div class="wx-panel">
<div class="wx-panel-header">
<span class="wx-panel-icon">&#9733;</span>
<span class="wx-panel-label">WALLET SCORE</span>
</div>
<div class="wx-panel-body" id="wx-score-body">
<div class="wx-section-loading" id="wx-score-loading">
<div class="wx-mini-spinner"></div>
<span>Calculating wallet score...</span>
</div>
<div id="wx-score-content"></div>
</div>
</div>
</div>
</div>
<footer class="footer">
<div class="footer-container">
<div class="footer-left">
<span class="footer-logo">[JAE]</span>
<span class="footer-copy">&copy; 2026 JAESWIFT.XYZ</span>
</div>
<div class="footer-right">
<span class="footer-signal">SIGNAL &#9608;&#9608;&#9608;&#9608;<span class="signal-flicker">&#9608;</span></span>
</div>
</div>
</footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/clock.js"></script>
<script src="/js/walletxray.js"></script>
</body>
</html>