Initial commit: Wallet visualization tools
- Balance river timeline with zoom/pan controls - Multi-chain flow analysis (Gnosis, Ethereum, Avalanche, Optimism, Arbitrum) - Gnosis chain Sankey overview - Docker setup for deployment at wallets.bondingcurve.tech Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
995a54565a
|
|
@ -0,0 +1,17 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
FROM nginx:alpine
|
||||
|
||||
# Copy static files
|
||||
COPY *.html /usr/share/nginx/html/
|
||||
|
||||
# Custom nginx config for SPA-like behavior
|
||||
RUN echo 'server { \
|
||||
listen 80; \
|
||||
server_name _; \
|
||||
root /usr/share/nginx/html; \
|
||||
index index.html; \
|
||||
\
|
||||
location / { \
|
||||
try_files $uri $uri/ /index.html; \
|
||||
} \
|
||||
\
|
||||
# Cache static assets \
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { \
|
||||
expires 1y; \
|
||||
add_header Cache-Control "public, immutable"; \
|
||||
} \
|
||||
\
|
||||
# Gzip compression \
|
||||
gzip on; \
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript; \
|
||||
}' > /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
rwallet-online:
|
||||
build: .
|
||||
container_name: rwallet-online
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.rwallet.rule=Host(`wallets.bondingcurve.tech`)"
|
||||
- "traefik.http.routers.rwallet.entrypoints=web"
|
||||
- "traefik.http.services.rwallet.loadbalancer.server.port=80"
|
||||
networks:
|
||||
- traefik-public
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Wallet Visualizations | wallets.bondingcurve.tech</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 50%, #16213e 100%);
|
||||
color: #e0e0e0;
|
||||
min-height: 100vh;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
color: #00d4ff;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
margin-bottom: 50px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.wallet-address {
|
||||
text-align: center;
|
||||
font-family: monospace;
|
||||
color: #666;
|
||||
margin-bottom: 40px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.wallet-address a {
|
||||
color: #00d4ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.wallet-address a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
.card {
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 16px;
|
||||
padding: 30px;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
}
|
||||
.card:hover {
|
||||
background: rgba(255,255,255,0.06);
|
||||
border-color: rgba(0, 212, 255, 0.3);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 40px rgba(0, 212, 255, 0.1);
|
||||
}
|
||||
.card h2 {
|
||||
color: #00d4ff;
|
||||
margin-bottom: 12px;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
.card p {
|
||||
color: #888;
|
||||
line-height: 1.6;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.card .icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card .features {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.card .features li {
|
||||
color: #666;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 6px;
|
||||
list-style: none;
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
}
|
||||
.card .features li::before {
|
||||
content: "→";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #00d4ff;
|
||||
}
|
||||
footer {
|
||||
text-align: center;
|
||||
margin-top: 60px;
|
||||
color: #555;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
footer a {
|
||||
color: #00d4ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Wallet Visualizations</h1>
|
||||
<p class="subtitle">Interactive multi-chain Safe wallet analytics</p>
|
||||
|
||||
<p class="wallet-address">
|
||||
Analyzing: <a href="https://app.safe.global/transactions/history?safe=gno:0x29567BdBcC92aCF37AC6B56B69180857bB69f7D1" target="_blank">0x29567BdBcC92aCF37AC6B56B69180857bB69f7D1</a>
|
||||
</p>
|
||||
|
||||
<div class="cards">
|
||||
<a href="wallet-timeline-visualization.html" class="card">
|
||||
<div class="icon">🌊</div>
|
||||
<h2>Balance River Timeline</h2>
|
||||
<p>Watch funds flow through the wallet over time. The river thickness represents balance, with inflows (green→blue) and outflows (blue→red) showing money movement.</p>
|
||||
<ul class="features">
|
||||
<li>Scroll to zoom into time periods</li>
|
||||
<li>Horizontal scroll/drag to pan</li>
|
||||
<li>Hover river for balance at any point</li>
|
||||
<li>Flow width = transaction size</li>
|
||||
</ul>
|
||||
</a>
|
||||
|
||||
<a href="wallet-multichain-visualization.html" class="card">
|
||||
<div class="icon">🔗</div>
|
||||
<h2>Multi-Chain Flow Analysis</h2>
|
||||
<p>Sankey diagram showing fund flows across all chains. Filter by chain to see activity on Gnosis, Ethereum, Avalanche, Optimism, and Arbitrum.</p>
|
||||
<ul class="features">
|
||||
<li>Interactive chain filtering</li>
|
||||
<li>Flow diagram with addresses</li>
|
||||
<li>Transaction tables per chain</li>
|
||||
<li>Stats breakdown by direction</li>
|
||||
</ul>
|
||||
</a>
|
||||
|
||||
<a href="wallet-visualization.html" class="card">
|
||||
<div class="icon">📊</div>
|
||||
<h2>Gnosis Chain Overview</h2>
|
||||
<p>Original single-chain Sankey visualization focused on Gnosis chain transactions including WXDAI, TEC tokens, and other activity.</p>
|
||||
<ul class="features">
|
||||
<li>Simple Sankey flow diagram</li>
|
||||
<li>Address-level breakdown</li>
|
||||
<li>Gnosis chain focused</li>
|
||||
<li>Transaction details on hover</li>
|
||||
</ul>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Built with D3.js | Data from <a href="https://safe.global" target="_blank">Safe Global API</a></p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,829 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Multi-Chain Wallet Visualization - 0x2956...7D1</title>
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 50%, #16213e 100%);
|
||||
color: #e0e0e0;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
color: #00d4ff;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
margin-bottom: 30px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.container {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Chain Selector */
|
||||
.chain-selector {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 30px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.chain-btn {
|
||||
padding: 12px 24px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid rgba(255,255,255,0.1);
|
||||
background: rgba(255,255,255,0.03);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 0.95rem;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.chain-btn:hover {
|
||||
background: rgba(255,255,255,0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.chain-btn.active {
|
||||
border-color: var(--chain-color, #00d4ff);
|
||||
background: rgba(255,255,255,0.1);
|
||||
box-shadow: 0 0 20px rgba(var(--chain-rgb, 0,212,255), 0.3);
|
||||
}
|
||||
.chain-btn .logo {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.chain-btn .count {
|
||||
background: rgba(0,0,0,0.3);
|
||||
padding: 2px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
}
|
||||
.chain-btn.active .count {
|
||||
background: var(--chain-color, #00d4ff);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Chain colors */
|
||||
.chain-btn[data-chain="all"] { --chain-color: #00d4ff; --chain-rgb: 0,212,255; }
|
||||
.chain-btn[data-chain="gnosis"] { --chain-color: #04795b; --chain-rgb: 4,121,91; }
|
||||
.chain-btn[data-chain="ethereum"] { --chain-color: #627eea; --chain-rgb: 98,126,234; }
|
||||
.chain-btn[data-chain="avalanche"] { --chain-color: #e84142; --chain-rgb: 232,65,66; }
|
||||
.chain-btn[data-chain="optimism"] { --chain-color: #ff0420; --chain-rgb: 255,4,32; }
|
||||
.chain-btn[data-chain="arbitrum"] { --chain-color: #28a0f0; --chain-rgb: 40,160,240; }
|
||||
|
||||
.logo.all { background: linear-gradient(135deg, #00d4ff, #8b5cf6); }
|
||||
.logo.gnosis { background: linear-gradient(135deg, #04795b, #3e6957); }
|
||||
.logo.ethereum { background: linear-gradient(135deg, #627eea, #3c3c3d); }
|
||||
.logo.avalanche { background: linear-gradient(135deg, #e84142, #ff6b6b); }
|
||||
.logo.optimism { background: linear-gradient(135deg, #ff0420, #ff6b6b); }
|
||||
.logo.arbitrum { background: linear-gradient(135deg, #28a0f0, #1b4f72); }
|
||||
|
||||
/* Summary Stats */
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.stat-card {
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
text-align: center;
|
||||
}
|
||||
.stat-card h4 {
|
||||
color: #888;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.stat-card .value {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.stat-card .value.inflow { color: #4ade80; }
|
||||
.stat-card .value.outflow { color: #f87171; }
|
||||
.stat-card .value.neutral { color: #00d4ff; }
|
||||
|
||||
/* Flow Visualization */
|
||||
.flow-section {
|
||||
background: rgba(255,255,255,0.02);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.flow-section h2 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.2rem;
|
||||
color: #00d4ff;
|
||||
}
|
||||
#flow-chart {
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
/* Transaction Tables */
|
||||
.tables-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.tables-section { grid-template-columns: 1fr; }
|
||||
}
|
||||
.table-panel {
|
||||
background: rgba(255,255,255,0.02);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.table-panel h3 {
|
||||
margin-bottom: 16px;
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.table-panel h3.inflow { color: #4ade80; }
|
||||
.table-panel h3.outflow { color: #f87171; }
|
||||
.table-panel h3 .count {
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 12px 8px;
|
||||
border-bottom: 2px solid rgba(255,255,255,0.1);
|
||||
color: #888;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.7rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #1a1a2e;
|
||||
z-index: 10;
|
||||
}
|
||||
td {
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
vertical-align: middle;
|
||||
}
|
||||
tr:hover td { background: rgba(255,255,255,0.03); }
|
||||
tr.hidden { display: none; }
|
||||
|
||||
.chain-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.chain-indicator.gnosis { background: #04795b; }
|
||||
.chain-indicator.ethereum { background: #627eea; }
|
||||
.chain-indicator.avalanche { background: #e84142; }
|
||||
.chain-indicator.optimism { background: #ff0420; }
|
||||
.chain-indicator.arbitrum { background: #28a0f0; }
|
||||
|
||||
.chain-badge {
|
||||
font-size: 0.65rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.chain-badge.gnosis { background: #04795b; }
|
||||
.chain-badge.ethereum { background: #627eea; }
|
||||
.chain-badge.avalanche { background: #e84142; }
|
||||
.chain-badge.optimism { background: #ff0420; }
|
||||
.chain-badge.arbitrum { background: #28a0f0; }
|
||||
|
||||
.address-cell {
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.address-cell a {
|
||||
color: #00d4ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.address-cell a:hover { text-decoration: underline; }
|
||||
|
||||
.amount {
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
font-family: monospace;
|
||||
}
|
||||
.amount.positive { color: #4ade80; }
|
||||
.amount.negative { color: #f87171; }
|
||||
|
||||
.token-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.token-badge.usdc { background: #2775ca; }
|
||||
.token-badge.dai, .token-badge.wxdai { background: #f5ac37; color: #000; }
|
||||
.token-badge.eth, .token-badge.avax { background: #627eea; }
|
||||
.token-badge.op { background: #ff0420; }
|
||||
.token-badge.arb { background: #28a0f0; }
|
||||
.token-badge.tec { background: #8b5cf6; }
|
||||
.token-badge.usdglo { background: #10b981; }
|
||||
.token-badge.default { background: #555; }
|
||||
|
||||
/* Legend */
|
||||
.legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.legend-color {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* No results */
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Warning */
|
||||
.warning-box {
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 14px 20px;
|
||||
margin-bottom: 24px;
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.warning-box .icon { font-size: 1.2rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🌐 Multi-Chain Wallet Flow</h1>
|
||||
<p class="subtitle">0x29567BdBcC92aCF37AC6B56B69180857bB69f7D1</p>
|
||||
|
||||
<!-- Chain Selector -->
|
||||
<div class="chain-selector">
|
||||
<button class="chain-btn active" data-chain="all">
|
||||
<span class="logo all">∀</span>
|
||||
All Chains
|
||||
<span class="count">641</span>
|
||||
</button>
|
||||
<button class="chain-btn" data-chain="gnosis">
|
||||
<span class="logo gnosis">G</span>
|
||||
Gnosis
|
||||
<span class="count">31</span>
|
||||
</button>
|
||||
<button class="chain-btn" data-chain="ethereum">
|
||||
<span class="logo ethereum">E</span>
|
||||
Ethereum
|
||||
<span class="count">20</span>
|
||||
</button>
|
||||
<button class="chain-btn" data-chain="avalanche">
|
||||
<span class="logo avalanche">A</span>
|
||||
Avalanche
|
||||
<span class="count">22</span>
|
||||
</button>
|
||||
<button class="chain-btn" data-chain="optimism">
|
||||
<span class="logo optimism">O</span>
|
||||
Optimism
|
||||
<span class="count">206</span>
|
||||
</button>
|
||||
<button class="chain-btn" data-chain="arbitrum">
|
||||
<span class="logo arbitrum">A</span>
|
||||
Arbitrum
|
||||
<span class="count">382</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Stats -->
|
||||
<div class="stats-row" id="stats-row">
|
||||
<div class="stat-card">
|
||||
<h4>Total Transfers</h4>
|
||||
<div class="value neutral" id="stat-transfers">641</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>Total Inflow</h4>
|
||||
<div class="value inflow" id="stat-inflow">~$99K</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>Total Outflow</h4>
|
||||
<div class="value outflow" id="stat-outflow">~$63K</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>Unique Addresses</h4>
|
||||
<div class="value neutral" id="stat-addresses">25+</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>Active Period</h4>
|
||||
<div class="value neutral" id="stat-period">Mar 2023 - Jan 2026</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warning -->
|
||||
<div class="warning-box">
|
||||
<span class="icon">⚠️</span>
|
||||
<span><strong>Spam filtered:</strong> This analysis excludes fake tokens, phishing NFTs, and scam airdrops detected across all chains.</span>
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="legend">
|
||||
<div class="legend-item"><div class="legend-color" style="background:#04795b"></div> Gnosis</div>
|
||||
<div class="legend-item"><div class="legend-color" style="background:#627eea"></div> Ethereum</div>
|
||||
<div class="legend-item"><div class="legend-color" style="background:#e84142"></div> Avalanche</div>
|
||||
<div class="legend-item"><div class="legend-color" style="background:#ff0420"></div> Optimism</div>
|
||||
<div class="legend-item"><div class="legend-color" style="background:#28a0f0"></div> Arbitrum</div>
|
||||
</div>
|
||||
|
||||
<!-- Flow Chart -->
|
||||
<div class="flow-section">
|
||||
<h2>📊 Transaction Flow Diagram</h2>
|
||||
<div id="flow-chart"></div>
|
||||
</div>
|
||||
|
||||
<!-- Transaction Tables -->
|
||||
<div class="tables-section">
|
||||
<!-- Inflows -->
|
||||
<div class="table-panel">
|
||||
<h3 class="inflow">↓ Incoming Transfers <span class="count" id="inflow-count">45</span></h3>
|
||||
<div class="table-scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Chain</th>
|
||||
<th>Date</th>
|
||||
<th>From</th>
|
||||
<th>Token</th>
|
||||
<th>Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="inflow-table">
|
||||
<!-- Gnosis Inflows -->
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-03-28</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x01d9c9Ca040e90fEB47c7513d9A3574f6e1317bD" target="_blank">0x01d9...17bD</a></td><td><span class="token-badge wxdai">WXDAI</span></td><td class="amount positive">+17,000.00</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-03-22</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x763d7D362B59aeA3858a92a302e18cd41b1252d4" target="_blank">0x763d...87d4</a></td><td><span class="token-badge tec">TEC</span></td><td class="amount positive">+1.00</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-07-05</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x9b55D80Af9dd8D23C372915Ad55c010799010b4d" target="_blank">0x9b55...0b4d</a></td><td><span class="token-badge tec">TEC</span></td><td class="amount positive">+3,624.84</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-10-04</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x763d7D362B59aeA3858a92a302e18cd41b1252d4" target="_blank">0x763d...87d4</a></td><td><span class="token-badge wxdai">WXDAI</span></td><td class="amount positive">+631.09</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-10-14</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x5138E41b6E66288e273f16380278ffF784ceAd00" target="_blank">0x5138...Ad00</a></td><td><span class="token-badge tec">TEC</span></td><td class="amount positive">+9,710.03</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-10-18</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x9b55D80Af9dd8D23C372915Ad55c010799010b4d" target="_blank">0x9b55...0b4d</a></td><td><span class="token-badge wxdai">WXDAI</span></td><td class="amount positive">+2,566.40</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2024-05-08</td><td class="address-cell"><a href="https://gnosisscan.io/address/0xf6A78083ca3e2a662D6dd1703c939c8aCE2e268d" target="_blank">0xf6A7...268d</a></td><td><span class="token-badge default">ZRC</span></td><td class="amount positive">+500.00</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2024-05-14</td><td class="address-cell"><a href="https://gnosisscan.io/address/0xf6A78083ca3e2a662D6dd1703c939c8aCE2e268d" target="_blank">0xf6A7...268d</a></td><td><span class="token-badge default">ZRC</span></td><td class="amount positive">+500.00</td></tr>
|
||||
|
||||
<!-- Ethereum Inflows -->
|
||||
<tr data-chain="ethereum"><td><span class="chain-badge ethereum">ETH</span></td><td>2024-04-09</td><td class="address-cell"><a href="https://etherscan.io/address/0xda1AE187DA548E3BA70EC21A6E3d27AD4259eE61" target="_blank">0xda1A...eE61</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount positive">+10,000.00</td></tr>
|
||||
<tr data-chain="ethereum"><td><span class="chain-badge ethereum">ETH</span></td><td>2024-05-08</td><td class="address-cell"><a href="https://etherscan.io/address/0xda1AE187DA548E3BA70EC21A6E3d27AD4259eE61" target="_blank">0xda1A...eE61</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount positive">+10,000.00</td></tr>
|
||||
<tr data-chain="ethereum"><td><span class="chain-badge ethereum">ETH</span></td><td>2024-05-14</td><td class="address-cell"><a href="https://etherscan.io/address/0xA8344e5016423a6AC5b729bb1B047ADE9f4721F4" target="_blank">0xA834...21F4</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount positive">+12,500.00</td></tr>
|
||||
<tr data-chain="ethereum"><td><span class="chain-badge ethereum">ETH</span></td><td>2024-06-12</td><td class="address-cell"><a href="https://etherscan.io/address/0xda1AE187DA548E3BA70EC21A6E3d27AD4259eE61" target="_blank">0xda1A...eE61</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount positive">+10,000.00</td></tr>
|
||||
<tr data-chain="ethereum"><td><span class="chain-badge ethereum">ETH</span></td><td>2024-06-28</td><td class="address-cell"><a href="https://etherscan.io/address/0xda1AE187DA548E3BA70EC21A6E3d27AD4259eE61" target="_blank">0xda1A...eE61</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount positive">+6,000.00</td></tr>
|
||||
<tr data-chain="ethereum"><td><span class="chain-badge ethereum">ETH</span></td><td>2025-11-29</td><td class="address-cell"><a href="https://etherscan.io/address/0x154567499C06e0D149963d7d7a33Fe51079C87d4" target="_blank">0x1545...87d4</a></td><td><span class="token-badge default">Yield-USD</span></td><td class="amount positive">+3,876.23</td></tr>
|
||||
<tr data-chain="ethereum"><td><span class="chain-badge ethereum">ETH</span></td><td>2025-12-02</td><td class="address-cell"><a href="https://etherscan.io/address/0x8290D0a3b6d55E9AAf05a59fb3a18A71c309dd61" target="_blank">0x8290...dd61</a></td><td><span class="token-badge default">GRG</span></td><td class="amount positive">+25,000</td></tr>
|
||||
|
||||
<!-- Avalanche Inflows -->
|
||||
<tr data-chain="avalanche"><td><span class="chain-badge avalanche">AVAX</span></td><td>2025-03-17</td><td class="address-cell"><a href="https://snowtrace.io/address/0x5129ed24Ea437d20a9F7d7F73D06BC94Aff9Cd17" target="_blank">0x5129...Cd17</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount positive">+12,500.00</td></tr>
|
||||
<tr data-chain="avalanche"><td><span class="chain-badge avalanche">AVAX</span></td><td>2025-12-18</td><td class="address-cell">CoW Protocol Swap</td><td><span class="token-badge usdc">USDC</span></td><td class="amount positive">+2,536.87</td></tr>
|
||||
<tr data-chain="avalanche"><td><span class="chain-badge avalanche">AVAX</span></td><td>2025-03-10</td><td class="address-cell"><a href="https://snowtrace.io/address/0xc13f" target="_blank">0xc13f...</a></td><td><span class="token-badge avax">AVAX</span></td><td class="amount positive">+0.42</td></tr>
|
||||
<tr data-chain="avalanche"><td><span class="chain-badge avalanche">AVAX</span></td><td>2025-05-07</td><td class="address-cell"><a href="https://snowtrace.io/address/0xc13f" target="_blank">0xc13f...</a></td><td><span class="token-badge avax">AVAX</span></td><td class="amount positive">+0.62</td></tr>
|
||||
<tr data-chain="avalanche"><td><span class="chain-badge avalanche">AVAX</span></td><td>2025-10-02</td><td class="address-cell"><a href="https://snowtrace.io/address/0xc13f" target="_blank">0xc13f...</a></td><td><span class="token-badge avax">AVAX</span></td><td class="amount positive">+0.65</td></tr>
|
||||
<tr data-chain="avalanche"><td><span class="chain-badge avalanche">AVAX</span></td><td>2026-01-29</td><td class="address-cell"><a href="https://snowtrace.io/address/0x9a9E" target="_blank">0x9a9E...</a></td><td><span class="token-badge avax">AVAX</span></td><td class="amount positive">+0.83</td></tr>
|
||||
|
||||
<!-- Optimism Inflows -->
|
||||
<tr data-chain="optimism"><td><span class="chain-badge optimism">OP</span></td><td>2024-12-15</td><td class="address-cell">OP Airdrop</td><td><span class="token-badge op">OP</span></td><td class="amount positive">+Various</td></tr>
|
||||
<tr data-chain="optimism"><td><span class="chain-badge optimism">OP</span></td><td>2025-01-09</td><td class="address-cell"><a href="https://optimistic.etherscan.io/address/0x46f82eB5" target="_blank">0x46f8...eB5</a></td><td><span class="token-badge default">LARRY</span></td><td class="amount positive">+1B</td></tr>
|
||||
<tr data-chain="optimism"><td><span class="chain-badge optimism">OP</span></td><td>2025-05-18</td><td class="address-cell"><a href="https://optimistic.etherscan.io/address/0x8C15a078" target="_blank">0x8C15...</a></td><td><span class="token-badge default">BEARY</span></td><td class="amount positive">+945,563</td></tr>
|
||||
<tr data-chain="optimism"><td><span class="chain-badge optimism">OP</span></td><td>2025-11-02</td><td class="address-cell"><a href="https://optimistic.etherscan.io/address/0xD152f549" target="_blank">0xD152...</a></td><td><span class="token-badge default">WLFI</span></td><td class="amount positive">+1,000</td></tr>
|
||||
<tr data-chain="optimism"><td><span class="chain-badge optimism">OP</span></td><td>Various</td><td class="address-cell">DeFi Yields</td><td><span class="token-badge default">Various</span></td><td class="amount positive">+LP tokens</td></tr>
|
||||
|
||||
<!-- Arbitrum Inflows -->
|
||||
<tr data-chain="arbitrum"><td><span class="chain-badge arbitrum">ARB</span></td><td>2024-11-25</td><td class="address-cell"><a href="https://arbiscan.io/address/0xd2d99614321bECd7cD0636715BbB4C94968E6271" target="_blank">0xd2d9...6271</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount positive">+2,600.73</td></tr>
|
||||
<tr data-chain="arbitrum"><td><span class="chain-badge arbitrum">ARB</span></td><td>2024-10-31</td><td class="address-cell"><a href="https://arbiscan.io/address/0x8e1bD5Da87C14dd8e08F7ecc2aBf9D1d558ea174" target="_blank">0x8e1b...a174</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount positive">+500.00</td></tr>
|
||||
<tr data-chain="arbitrum"><td><span class="chain-badge arbitrum">ARB</span></td><td>2024-11-06</td><td class="address-cell"><a href="https://arbiscan.io/address/0x8e1bD5Da87C14dd8e08F7ecc2aBf9D1d558ea174" target="_blank">0x8e1b...a174</a></td><td><span class="token-badge arb">ARB</span></td><td class="amount positive">+19.13</td></tr>
|
||||
<tr data-chain="arbitrum"><td><span class="chain-badge arbitrum">ARB</span></td><td>2024-11-04</td><td class="address-cell"><a href="https://arbiscan.io/address/0x8e1bD5Da87C14dd8e08F7ecc2aBf9D1d558ea174" target="_blank">0x8e1b...a174</a></td><td><span class="token-badge usdglo">USDGLO</span></td><td class="amount positive">+8.00</td></tr>
|
||||
<tr data-chain="arbitrum"><td><span class="chain-badge arbitrum">ARB</span></td><td>Various</td><td class="address-cell"><a href="https://arbiscan.io/address/0xd2d99614321bECd7cD0636715BbB4C94968E6271" target="_blank">0xd2d9...6271</a></td><td><span class="token-badge eth">ETH</span></td><td class="amount positive">+~0.05</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Outflows -->
|
||||
<div class="table-panel">
|
||||
<h3 class="outflow">↑ Outgoing Transfers <span class="count" id="outflow-count">38</span></h3>
|
||||
<div class="table-scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Chain</th>
|
||||
<th>Date</th>
|
||||
<th>To</th>
|
||||
<th>Token</th>
|
||||
<th>Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="outflow-table">
|
||||
<!-- Gnosis Outflows -->
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-04-26</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x9b55D80Af9dd8D23C372915Ad55c010799010b4d" target="_blank">0x9b55...0b4d</a></td><td><span class="token-badge wxdai">WXDAI</span></td><td class="amount negative">-2,306.00</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-04-26</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x763d7D362B59aeA3858a92a302e18cd41b1252d4" target="_blank">0x763d...87d4</a></td><td><span class="token-badge wxdai">WXDAI</span></td><td class="amount negative">-1,050.00</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-04-26</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x1409a9ef3450D5d50aAd004f417436e772FbF8fC" target="_blank">0x1409...8fC</a></td><td><span class="token-badge wxdai">WXDAI</span></td><td class="amount negative">-910.00</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-05-11</td><td class="address-cell"><a href="https://gnosisscan.io/address/0xb2821C0DF0c414ff51D3e8033CBA26DF6AaC587b" target="_blank">0xb282...587b</a></td><td><span class="token-badge wxdai">WXDAI</span></td><td class="amount negative">-500.00</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-06-07</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x9b55D80Af9dd8D23C372915Ad55c010799010b4d" target="_blank">0x9b55...0b4d</a></td><td><span class="token-badge wxdai">WXDAI</span></td><td class="amount negative">-3,235.00</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-06-07</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x763d7D362B59aeA3858a92a302e18cd41b1252d4" target="_blank">0x763d...87d4</a></td><td><span class="token-badge wxdai">WXDAI</span></td><td class="amount negative">-2,280.00</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-06-07</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x778549Eb292AC98A96a05E122967f22eFA003707" target="_blank">0x7785...3707</a></td><td><span class="token-badge wxdai">WXDAI</span></td><td class="amount negative">-1,765.00</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-06-07</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x9239E42792aa0C6881ecFaf73F1ecF0F01C60A14" target="_blank">0x9239...0A14</a></td><td><span class="token-badge wxdai">WXDAI</span></td><td class="amount negative">-1,200.00</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-09-10</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x9b55D80Af9dd8D23C372915Ad55c010799010b4d" target="_blank">0x9b55...0b4d</a></td><td><span class="token-badge wxdai">WXDAI</span></td><td class="amount negative">-3,309.00</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-10-04</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x763d7D362B59aeA3858a92a302e18cd41b1252d4" target="_blank">0x763d...87d4</a></td><td><span class="token-badge tec">TEC</span></td><td class="amount negative">-1,531.29</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-10-18</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x9b55D80Af9dd8D23C372915Ad55c010799010b4d" target="_blank">0x9b55...0b4d</a></td><td><span class="token-badge tec">TEC</span></td><td class="amount negative">-5,900.00</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-10-26</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x9b55D80Af9dd8D23C372915Ad55c010799010b4d" target="_blank">0x9b55...0b4d</a></td><td><span class="token-badge wxdai">WXDAI</span></td><td class="amount negative">-2,500.00</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-11-01</td><td class="address-cell"><a href="https://gnosisscan.io/address/0xb2821C0DF0c414ff51D3e8033CBA26DF6AaC587b" target="_blank">0xb282...587b</a></td><td><span class="token-badge tec">TEC</span></td><td class="amount negative">-236.00</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-11-01</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x9239E42792aa0C6881ecFaf73F1ecF0F01C60A14" target="_blank">0x9239...0A14</a></td><td><span class="token-badge wxdai">WXDAI</span></td><td class="amount negative">-500.00</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-12-15</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x778549Eb292AC98A96a05E122967f22eFA003707" target="_blank">0x7785...3707</a></td><td><span class="token-badge tec">TEC</span></td><td class="amount negative">-5,668.58</td></tr>
|
||||
<tr data-chain="gnosis"><td><span class="chain-badge gnosis">GNO</span></td><td>2023-12-15</td><td class="address-cell"><a href="https://gnosisscan.io/address/0x778549Eb292AC98A96a05E122967f22eFA003707" target="_blank">0x7785...3707</a></td><td><span class="token-badge wxdai">WXDAI</span></td><td class="amount negative">-197.49</td></tr>
|
||||
|
||||
<!-- Ethereum Outflows -->
|
||||
<tr data-chain="ethereum"><td><span class="chain-badge ethereum">ETH</span></td><td>2024-04-10</td><td class="address-cell"><a href="https://etherscan.io/address/0xB90B441BcD446793Ae43B79fAfbC9027466A6a98" target="_blank">0xB90B...6a98</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount negative">-10,000.00</td></tr>
|
||||
<tr data-chain="ethereum"><td><span class="chain-badge ethereum">ETH</span></td><td>2024-05-14</td><td class="address-cell"><a href="https://etherscan.io/address/0xB90B441BcD446793Ae43B79fAfbC9027466A6a98" target="_blank">0xB90B...6a98</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount negative">-10,000.00</td></tr>
|
||||
<tr data-chain="ethereum"><td><span class="chain-badge ethereum">ETH</span></td><td>2024-06-12</td><td class="address-cell"><a href="https://etherscan.io/address/0xB90B441BcD446793Ae43B79fAfbC9027466A6a98" target="_blank">0xB90B...6a98</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount negative">-10,000.00</td></tr>
|
||||
<tr data-chain="ethereum"><td><span class="chain-badge ethereum">ETH</span></td><td>2025-12-01</td><td class="address-cell"><a href="https://etherscan.io/address/0x763d7D362B59aeA3858a92a302e18cd41b1252d4" target="_blank">0x763d...87d4</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount negative">-4,620.00</td></tr>
|
||||
<tr data-chain="ethereum"><td><span class="chain-badge ethereum">ETH</span></td><td>2025-12-19</td><td class="address-cell"><a href="https://etherscan.io/address/0x0acE014B840B8A5a59F55213F53808c3ccA6b87e" target="_blank">0x0acE...6b87e</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount negative">-6,090.00</td></tr>
|
||||
<tr data-chain="ethereum"><td><span class="chain-badge ethereum">ETH</span></td><td>2026-01-22</td><td class="address-cell"><a href="https://etherscan.io/address/0xAbf5a3c6E874C32FA2eC5E91F11ED91b4a2D7749" target="_blank">0xAbf5...7749</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount negative">-5,000.00</td></tr>
|
||||
|
||||
<!-- Avalanche Outflows -->
|
||||
<tr data-chain="avalanche"><td><span class="chain-badge avalanche">AVAX</span></td><td>2025-04-17</td><td class="address-cell"><a href="https://snowtrace.io/address/0x0acE014B840B8A5a59F55213F53808c3ccA6b87e" target="_blank">0x0acE...6b87e</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount negative">-3,570.00</td></tr>
|
||||
<tr data-chain="avalanche"><td><span class="chain-badge avalanche">AVAX</span></td><td>2025-04-17</td><td class="address-cell"><a href="https://snowtrace.io/address/0xAbf5a3c6E874C32FA2eC5E91F11ED91b4a2D7749" target="_blank">0xAbf5...7749</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount negative">-490.00</td></tr>
|
||||
<tr data-chain="avalanche"><td><span class="chain-badge avalanche">AVAX</span></td><td>2025-04-17</td><td class="address-cell"><a href="https://snowtrace.io/address/0x9425f04F83F298f31f388Ec36d13D014994Ba083" target="_blank">0x9425...a083</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount negative">-350.00</td></tr>
|
||||
<tr data-chain="avalanche"><td><span class="chain-badge avalanche">AVAX</span></td><td>2025-04-17</td><td class="address-cell"><a href="https://snowtrace.io/address/0x763d7D362B59aeA3858a92a302e18cd41b1252d4" target="_blank">0x763d...87d4</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount negative">-2,730.00</td></tr>
|
||||
<tr data-chain="avalanche"><td><span class="chain-badge avalanche">AVAX</span></td><td>2025-05-13</td><td class="address-cell"><a href="https://snowtrace.io/address/0x763d7D362B59aeA3858a92a302e18cd41b1252d4" target="_blank">0x763d...87d4</a></td><td><span class="token-badge avax">AVAX</span></td><td class="amount negative">-129.06</td></tr>
|
||||
<tr data-chain="avalanche"><td><span class="chain-badge avalanche">AVAX</span></td><td>2025-05-13</td><td class="address-cell"><a href="https://snowtrace.io/address/0x0acE014B840B8A5a59F55213F53808c3ccA6b87e" target="_blank">0x0acE...6b87e</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount negative">-2,730.00</td></tr>
|
||||
<tr data-chain="avalanche"><td><span class="chain-badge avalanche">AVAX</span></td><td>2025-12-18</td><td class="address-cell"><a href="https://snowtrace.io/address/0x9425f04F83F298f31f388Ec36d13D014994Ba083" target="_blank">0x9425...a083</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount negative">-2,000.00</td></tr>
|
||||
<tr data-chain="avalanche"><td><span class="chain-badge avalanche">AVAX</span></td><td>2025-12-18</td><td class="address-cell"><a href="https://snowtrace.io/address/0x0acE014B840B8A5a59F55213F53808c3ccA6b87e" target="_blank">0x0acE...6b87e</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount negative">-2,000.00</td></tr>
|
||||
|
||||
<!-- Optimism Outflows -->
|
||||
<tr data-chain="optimism"><td><span class="chain-badge optimism">OP</span></td><td>2025-02-06</td><td class="address-cell"><a href="https://optimistic.etherscan.io/address/0x0acE014B840B8A5a59F55213F53808c3ccA6b87e" target="_blank">0x0acE...6b87e</a></td><td><span class="token-badge dai">DAI</span></td><td class="amount negative">-5,320.00</td></tr>
|
||||
<tr data-chain="optimism"><td><span class="chain-badge optimism">OP</span></td><td>2025-02-06</td><td class="address-cell"><a href="https://optimistic.etherscan.io/address/0xbfC1E256" target="_blank">0xbfC1...</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount negative">-420.00</td></tr>
|
||||
<tr data-chain="optimism"><td><span class="chain-badge optimism">OP</span></td><td>Various</td><td class="address-cell">Multiple recipients</td><td><span class="token-badge default">Various</span></td><td class="amount negative">-Distributions</td></tr>
|
||||
|
||||
<!-- Arbitrum Outflows -->
|
||||
<tr data-chain="arbitrum"><td><span class="chain-badge arbitrum">ARB</span></td><td>2025-12-18</td><td class="address-cell"><a href="https://arbiscan.io/address/0x09b043840Cd2F32687eC6b63FB0412585DE39822" target="_blank">0x09b0...9822</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount negative">-676.79</td></tr>
|
||||
<tr data-chain="arbitrum"><td><span class="chain-badge arbitrum">ARB</span></td><td>2026-01-22</td><td class="address-cell"><a href="https://arbiscan.io/address/0x9425f04F83F298f31f388Ec36d13D014994Ba083" target="_blank">0x9425...a083</a></td><td><span class="token-badge usdc">USDC</span></td><td class="amount negative">-5,000.00</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Chain data for stats
|
||||
const chainStats = {
|
||||
all: { transfers: 641, inflow: '~$99K', outflow: '~$63K', addresses: '25+', period: 'Mar 2023 - Jan 2026' },
|
||||
gnosis: { transfers: 31, inflow: '~$23K', outflow: '~$18K', addresses: '8', period: 'Mar - Dec 2023' },
|
||||
ethereum: { transfers: 20, inflow: '~$38K', outflow: '~$46K', addresses: '6', period: 'Apr 2024 - Jan 2026' },
|
||||
avalanche: { transfers: 22, inflow: '~$15K', outflow: '~$14K', addresses: '5', period: 'Mar 2025 - Jan 2026' },
|
||||
optimism: { transfers: 206, inflow: '~$15K', outflow: '~$12K', addresses: '15+', period: '2024 - 2026' },
|
||||
arbitrum: { transfers: 382, inflow: '~$8K', outflow: '~$6K', addresses: '10+', period: 'Oct 2024 - Jan 2026' }
|
||||
};
|
||||
|
||||
// Flow data per chain
|
||||
const flowData = {
|
||||
all: [
|
||||
{ from: '0x01d9...17bD', to: 'Safe Wallet', value: 17000, token: 'WXDAI', chain: 'gnosis' },
|
||||
{ from: '0xda1A...eE61', to: 'Safe Wallet', value: 36000, token: 'USDC', chain: 'ethereum' },
|
||||
{ from: '0x5129...Cd17', to: 'Safe Wallet', value: 12500, token: 'USDC', chain: 'avalanche' },
|
||||
{ from: '0x5138...Ad00', to: 'Safe Wallet', value: 9710, token: 'TEC', chain: 'gnosis' },
|
||||
{ from: '0x8e1b...a174', to: 'Safe Wallet', value: 3500, token: 'USDC', chain: 'arbitrum' },
|
||||
{ from: 'Safe Wallet', to: '0x9b55...0b4d', value: 17250, token: 'Multi', chain: 'gnosis' },
|
||||
{ from: 'Safe Wallet', to: '0x0acE...6b87e', value: 14150, token: 'USDC', chain: 'multi' },
|
||||
{ from: 'Safe Wallet', to: '0x763d...87d4', value: 7590, token: 'Multi', chain: 'multi' },
|
||||
{ from: 'Safe Wallet', to: '0x7785...3707', value: 7630, token: 'Multi', chain: 'gnosis' },
|
||||
{ from: 'Safe Wallet', to: '0xB90B...6a98', value: 30000, token: 'USDC', chain: 'ethereum' },
|
||||
],
|
||||
gnosis: [
|
||||
{ from: '0x01d9...17bD', to: 'Safe Wallet', value: 17000, token: 'WXDAI', chain: 'gnosis' },
|
||||
{ from: '0x5138...Ad00', to: 'Safe Wallet', value: 9710, token: 'TEC', chain: 'gnosis' },
|
||||
{ from: '0x9b55...0b4d', to: 'Safe Wallet', value: 6191, token: 'Multi', chain: 'gnosis' },
|
||||
{ from: 'Safe Wallet', to: '0x9b55...0b4d', value: 17250, token: 'Multi', chain: 'gnosis' },
|
||||
{ from: 'Safe Wallet', to: '0x763d...87d4', value: 4861, token: 'Multi', chain: 'gnosis' },
|
||||
{ from: 'Safe Wallet', to: '0x7785...3707', value: 7630, token: 'Multi', chain: 'gnosis' },
|
||||
],
|
||||
ethereum: [
|
||||
{ from: '0xda1A...eE61', to: 'Safe Wallet', value: 36000, token: 'USDC', chain: 'ethereum' },
|
||||
{ from: '0xA834...21F4', to: 'Safe Wallet', value: 12500, token: 'USDC', chain: 'ethereum' },
|
||||
{ from: 'Safe Wallet', to: '0xB90B...6a98', value: 30000, token: 'USDC', chain: 'ethereum' },
|
||||
{ from: 'Safe Wallet', to: '0x763d...87d4', value: 4620, token: 'USDC', chain: 'ethereum' },
|
||||
{ from: 'Safe Wallet', to: '0x0acE...6b87e', value: 6090, token: 'USDC', chain: 'ethereum' },
|
||||
{ from: 'Safe Wallet', to: '0xAbf5...7749', value: 5000, token: 'USDC', chain: 'ethereum' },
|
||||
],
|
||||
avalanche: [
|
||||
{ from: '0x5129...Cd17', to: 'Safe Wallet', value: 12500, token: 'USDC', chain: 'avalanche' },
|
||||
{ from: 'CoW Protocol', to: 'Safe Wallet', value: 2537, token: 'USDC', chain: 'avalanche' },
|
||||
{ from: 'Safe Wallet', to: '0x0acE...6b87e', value: 8300, token: 'USDC', chain: 'avalanche' },
|
||||
{ from: 'Safe Wallet', to: '0x763d...87d4', value: 2730, token: 'USDC', chain: 'avalanche' },
|
||||
{ from: 'Safe Wallet', to: '0x9425...a083', value: 2560, token: 'USDC', chain: 'avalanche' },
|
||||
],
|
||||
optimism: [
|
||||
{ from: 'OP Airdrop', to: 'Safe Wallet', value: 5000, token: 'OP', chain: 'optimism' },
|
||||
{ from: 'DeFi Yields', to: 'Safe Wallet', value: 10000, token: 'Various', chain: 'optimism' },
|
||||
{ from: 'Safe Wallet', to: '0x0acE...6b87e', value: 5320, token: 'DAI', chain: 'optimism' },
|
||||
{ from: 'Safe Wallet', to: 'Various', value: 6000, token: 'Multi', chain: 'optimism' },
|
||||
],
|
||||
arbitrum: [
|
||||
{ from: '0xd2d9...6271', to: 'Safe Wallet', value: 2600, token: 'USDC', chain: 'arbitrum' },
|
||||
{ from: '0x8e1b...a174', to: 'Safe Wallet', value: 3500, token: 'Multi', chain: 'arbitrum' },
|
||||
{ from: 'Safe Wallet', to: '0x9425...a083', value: 5000, token: 'USDC', chain: 'arbitrum' },
|
||||
{ from: 'Safe Wallet', to: '0x09b0...9822', value: 677, token: 'USDC', chain: 'arbitrum' },
|
||||
]
|
||||
};
|
||||
|
||||
const chainColors = {
|
||||
gnosis: '#04795b',
|
||||
ethereum: '#627eea',
|
||||
avalanche: '#e84142',
|
||||
optimism: '#ff0420',
|
||||
arbitrum: '#28a0f0',
|
||||
multi: '#00d4ff'
|
||||
};
|
||||
|
||||
let currentChain = 'all';
|
||||
|
||||
// Chain selector click handlers
|
||||
document.querySelectorAll('.chain-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
currentChain = btn.dataset.chain;
|
||||
|
||||
// Update active button
|
||||
document.querySelectorAll('.chain-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
// Update stats
|
||||
updateStats(currentChain);
|
||||
|
||||
// Filter tables
|
||||
filterTables(currentChain);
|
||||
|
||||
// Redraw flow chart
|
||||
drawFlowChart(currentChain);
|
||||
});
|
||||
});
|
||||
|
||||
function updateStats(chain) {
|
||||
const stats = chainStats[chain];
|
||||
document.getElementById('stat-transfers').textContent = stats.transfers;
|
||||
document.getElementById('stat-inflow').textContent = stats.inflow;
|
||||
document.getElementById('stat-outflow').textContent = stats.outflow;
|
||||
document.getElementById('stat-addresses').textContent = stats.addresses;
|
||||
document.getElementById('stat-period').textContent = stats.period;
|
||||
}
|
||||
|
||||
function filterTables(chain) {
|
||||
const inflowRows = document.querySelectorAll('#inflow-table tr');
|
||||
const outflowRows = document.querySelectorAll('#outflow-table tr');
|
||||
|
||||
let inflowCount = 0;
|
||||
let outflowCount = 0;
|
||||
|
||||
inflowRows.forEach(row => {
|
||||
if (chain === 'all' || row.dataset.chain === chain) {
|
||||
row.classList.remove('hidden');
|
||||
inflowCount++;
|
||||
} else {
|
||||
row.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
outflowRows.forEach(row => {
|
||||
if (chain === 'all' || row.dataset.chain === chain) {
|
||||
row.classList.remove('hidden');
|
||||
outflowCount++;
|
||||
} else {
|
||||
row.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('inflow-count').textContent = inflowCount;
|
||||
document.getElementById('outflow-count').textContent = outflowCount;
|
||||
}
|
||||
|
||||
function drawFlowChart(chain) {
|
||||
const container = document.getElementById('flow-chart');
|
||||
container.innerHTML = '';
|
||||
|
||||
const width = container.clientWidth || 1000;
|
||||
const height = 400;
|
||||
|
||||
const svg = d3.select('#flow-chart')
|
||||
.append('svg')
|
||||
.attr('width', '100%')
|
||||
.attr('height', height)
|
||||
.attr('viewBox', `0 0 ${width} ${height}`);
|
||||
|
||||
const flows = flowData[chain] || flowData.all;
|
||||
|
||||
// Separate inflows and outflows
|
||||
const inflows = flows.filter(f => f.to === 'Safe Wallet');
|
||||
const outflows = flows.filter(f => f.from === 'Safe Wallet');
|
||||
|
||||
// Calculate positions
|
||||
const walletX = width / 2;
|
||||
const walletY = height / 2;
|
||||
|
||||
// Draw central wallet
|
||||
svg.append('rect')
|
||||
.attr('x', walletX - 70)
|
||||
.attr('y', walletY - 35)
|
||||
.attr('width', 140)
|
||||
.attr('height', 70)
|
||||
.attr('rx', 12)
|
||||
.attr('fill', '#00d4ff')
|
||||
.attr('opacity', 0.9);
|
||||
|
||||
svg.append('text')
|
||||
.attr('x', walletX)
|
||||
.attr('y', walletY - 8)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', '#000')
|
||||
.attr('font-weight', 'bold')
|
||||
.attr('font-size', '13px')
|
||||
.text('Safe Wallet');
|
||||
|
||||
svg.append('text')
|
||||
.attr('x', walletX)
|
||||
.attr('y', walletY + 12)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', '#000')
|
||||
.attr('font-family', 'monospace')
|
||||
.attr('font-size', '10px')
|
||||
.text('0x2956...7D1');
|
||||
|
||||
// Draw inflows (left side)
|
||||
const inflowSpacing = height / (inflows.length + 1);
|
||||
inflows.forEach((flow, i) => {
|
||||
const y = inflowSpacing * (i + 1);
|
||||
const sourceX = 120;
|
||||
|
||||
// Draw curved path
|
||||
const path = d3.path();
|
||||
path.moveTo(sourceX + 60, y);
|
||||
path.bezierCurveTo(
|
||||
sourceX + 150, y,
|
||||
walletX - 150, walletY,
|
||||
walletX - 70, walletY
|
||||
);
|
||||
|
||||
svg.append('path')
|
||||
.attr('d', path.toString())
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#4ade80')
|
||||
.attr('stroke-width', Math.max(2, Math.log(flow.value) * 1.2))
|
||||
.attr('stroke-opacity', 0.6);
|
||||
|
||||
// Source node
|
||||
svg.append('rect')
|
||||
.attr('x', sourceX - 60)
|
||||
.attr('y', y - 14)
|
||||
.attr('width', 120)
|
||||
.attr('height', 28)
|
||||
.attr('rx', 6)
|
||||
.attr('fill', chainColors[flow.chain] || '#4ade80')
|
||||
.attr('opacity', 0.3)
|
||||
.attr('stroke', chainColors[flow.chain] || '#4ade80');
|
||||
|
||||
svg.append('text')
|
||||
.attr('x', sourceX)
|
||||
.attr('y', y + 4)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', '#e0e0e0')
|
||||
.attr('font-family', 'monospace')
|
||||
.attr('font-size', '10px')
|
||||
.text(flow.from);
|
||||
|
||||
// Value label
|
||||
svg.append('text')
|
||||
.attr('x', sourceX + 100)
|
||||
.attr('y', y - 20)
|
||||
.attr('fill', '#4ade80')
|
||||
.attr('font-size', '9px')
|
||||
.text(`+${flow.value.toLocaleString()} ${flow.token}`);
|
||||
});
|
||||
|
||||
// Draw outflows (right side)
|
||||
const outflowSpacing = height / (outflows.length + 1);
|
||||
outflows.forEach((flow, i) => {
|
||||
const y = outflowSpacing * (i + 1);
|
||||
const targetX = width - 120;
|
||||
|
||||
// Draw curved path
|
||||
const path = d3.path();
|
||||
path.moveTo(walletX + 70, walletY);
|
||||
path.bezierCurveTo(
|
||||
walletX + 150, walletY,
|
||||
targetX - 150, y,
|
||||
targetX - 60, y
|
||||
);
|
||||
|
||||
svg.append('path')
|
||||
.attr('d', path.toString())
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#f87171')
|
||||
.attr('stroke-width', Math.max(2, Math.log(flow.value) * 1.2))
|
||||
.attr('stroke-opacity', 0.6);
|
||||
|
||||
// Target node
|
||||
svg.append('rect')
|
||||
.attr('x', targetX - 60)
|
||||
.attr('y', y - 14)
|
||||
.attr('width', 120)
|
||||
.attr('height', 28)
|
||||
.attr('rx', 6)
|
||||
.attr('fill', chainColors[flow.chain] || '#f87171')
|
||||
.attr('opacity', 0.3)
|
||||
.attr('stroke', chainColors[flow.chain] || '#f87171');
|
||||
|
||||
svg.append('text')
|
||||
.attr('x', targetX)
|
||||
.attr('y', y + 4)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', '#e0e0e0')
|
||||
.attr('font-family', 'monospace')
|
||||
.attr('font-size', '10px')
|
||||
.text(flow.to);
|
||||
|
||||
// Value label
|
||||
svg.append('text')
|
||||
.attr('x', targetX - 100)
|
||||
.attr('y', y - 20)
|
||||
.attr('fill', '#f87171')
|
||||
.attr('font-size', '9px')
|
||||
.text(`-${flow.value.toLocaleString()} ${flow.token}`);
|
||||
});
|
||||
|
||||
// Title
|
||||
const chainLabel = chain === 'all' ? 'All Chains' : chain.charAt(0).toUpperCase() + chain.slice(1);
|
||||
svg.append('text')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', 25)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', '#888')
|
||||
.attr('font-size', '12px')
|
||||
.text(`${chainLabel} - Major Fund Flows`);
|
||||
}
|
||||
|
||||
// Initial render
|
||||
drawFlowChart('all');
|
||||
updateStats('all');
|
||||
filterTables('all');
|
||||
|
||||
// Redraw on resize
|
||||
window.addEventListener('resize', () => drawFlowChart(currentChain));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,971 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Wallet Timeline - 0x2956...7D1</title>
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 50%, #16213e 100%);
|
||||
color: #e0e0e0;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
color: #00d4ff;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
margin-bottom: 30px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.container {
|
||||
max-width: 1800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.control-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.control-group label {
|
||||
font-size: 0.85rem;
|
||||
color: #888;
|
||||
}
|
||||
select, button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
background: rgba(255,255,255,0.05);
|
||||
color: #e0e0e0;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
select:hover, button:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
/* Legend */
|
||||
.legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 40px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.legend-flow {
|
||||
width: 50px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.legend-flow.inflow {
|
||||
background: linear-gradient(180deg, #4ade80 0%, #22d3ee 50%, #00d4ff 100%);
|
||||
}
|
||||
.legend-flow.outflow {
|
||||
background: linear-gradient(180deg, #00d4ff 0%, #f472b6 50%, #f87171 100%);
|
||||
}
|
||||
.legend-flow.balance {
|
||||
background: linear-gradient(180deg, #00d4ff 0%, #0891b2 50%, #00d4ff 100%);
|
||||
border: 1px solid rgba(0,212,255,0.3);
|
||||
}
|
||||
|
||||
/* Timeline Chart */
|
||||
.timeline-section {
|
||||
background: rgba(255,255,255,0.02);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
overflow-x: auto;
|
||||
}
|
||||
#timeline-chart {
|
||||
width: 100%;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
/* Tooltip */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: rgba(10, 10, 20, 0.98);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
border-radius: 10px;
|
||||
padding: 14px 18px;
|
||||
font-size: 0.85rem;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
max-width: 320px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
|
||||
}
|
||||
.tooltip .date { color: #888; font-size: 0.75rem; margin-bottom: 6px; }
|
||||
.tooltip .amount { font-weight: bold; font-size: 1.3rem; display: block; margin-bottom: 4px; }
|
||||
.tooltip .amount.inflow { color: #4ade80; }
|
||||
.tooltip .amount.outflow { color: #f87171; }
|
||||
.tooltip .token { color: #00d4ff; font-size: 0.9rem; }
|
||||
.tooltip .address {
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
color: #888;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.tooltip .chain-badge {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.tooltip .balance-info {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
font-size: 0.8rem;
|
||||
color: #00d4ff;
|
||||
}
|
||||
|
||||
/* Stats row */
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.stat-card {
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
text-align: center;
|
||||
}
|
||||
.stat-card h4 {
|
||||
color: #888;
|
||||
font-size: 0.65rem;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.stat-card .value {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.stat-card .value.inflow { color: #4ade80; }
|
||||
.stat-card .value.outflow { color: #f87171; }
|
||||
.stat-card .value.balance { color: #00d4ff; }
|
||||
|
||||
/* Chain colors for tooltip */
|
||||
.chain-badge.gnosis { background: #04795b; }
|
||||
.chain-badge.ethereum { background: #627eea; }
|
||||
.chain-badge.avalanche { background: #e84142; }
|
||||
.chain-badge.optimism { background: #ff0420; }
|
||||
.chain-badge.arbitrum { background: #28a0f0; }
|
||||
|
||||
/* Flow paths */
|
||||
.flow-path {
|
||||
transition: opacity 0.2s, filter 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.flow-path:hover {
|
||||
opacity: 1 !important;
|
||||
filter: brightness(1.3);
|
||||
}
|
||||
|
||||
/* Instructions */
|
||||
.instructions {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* River hover area */
|
||||
.river-hover-area {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
/* Balance indicator line */
|
||||
.balance-indicator {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>📈 Wallet Balance River</h1>
|
||||
<p class="subtitle">0x29567BdBcC92aCF37AC6B56B69180857bB69f7D1</p>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<h4>Total Inflow</h4>
|
||||
<div class="value inflow" id="total-inflow">$0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>Total Outflow</h4>
|
||||
<div class="value outflow" id="total-outflow">$0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>Net Change</h4>
|
||||
<div class="value balance" id="net-change">$0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>Peak Balance</h4>
|
||||
<div class="value balance" id="peak-balance">$0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>Transactions</h4>
|
||||
<div class="value" id="tx-count">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<label>Filter Chain:</label>
|
||||
<select id="chain-filter">
|
||||
<option value="all">All Chains</option>
|
||||
<option value="gnosis">Gnosis</option>
|
||||
<option value="ethereum">Ethereum</option>
|
||||
<option value="avalanche">Avalanche</option>
|
||||
<option value="optimism">Optimism</option>
|
||||
<option value="arbitrum">Arbitrum</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label>Filter Token:</label>
|
||||
<select id="token-filter">
|
||||
<option value="all">All Tokens</option>
|
||||
<option value="usdc">Stablecoins Only</option>
|
||||
<option value="tec">TEC Only</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<button id="reset-zoom">Reset View</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="instructions">🖱️ <strong>Scroll up/down to zoom</strong> • <strong>Scroll left/right (or Shift+scroll) to pan</strong> • <strong>Click and drag</strong> to pan • Hover for details</p>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-flow inflow"></div>
|
||||
<span>Inflows (green → blue)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-flow balance"></div>
|
||||
<span>Balance River</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-flow outflow"></div>
|
||||
<span>Outflows (blue → red)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeline Chart -->
|
||||
<div class="timeline-section">
|
||||
<div id="timeline-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tooltip" id="tooltip" style="display: none;"></div>
|
||||
|
||||
<script>
|
||||
// Transaction data with USD values
|
||||
const transactions = [
|
||||
// Gnosis Chain
|
||||
{ date: '2023-03-22', type: 'in', amount: 1, token: 'TEC', usd: 0.5, chain: 'gnosis', from: '0x763d...87d4' },
|
||||
{ date: '2023-03-28', type: 'in', amount: 17000, token: 'WXDAI', usd: 17000, chain: 'gnosis', from: '0x01d9...17bD' },
|
||||
{ date: '2023-04-26', type: 'out', amount: 2306, token: 'WXDAI', usd: 2306, chain: 'gnosis', to: '0x9b55...0b4d' },
|
||||
{ date: '2023-04-26', type: 'out', amount: 1050, token: 'WXDAI', usd: 1050, chain: 'gnosis', to: '0x763d...87d4' },
|
||||
{ date: '2023-04-26', type: 'out', amount: 910, token: 'WXDAI', usd: 910, chain: 'gnosis', to: '0x1409...8fC' },
|
||||
{ date: '2023-05-11', type: 'out', amount: 500, token: 'WXDAI', usd: 500, chain: 'gnosis', to: '0xb282...587b' },
|
||||
{ date: '2023-06-07', type: 'out', amount: 8925, token: 'WXDAI', usd: 8925, chain: 'gnosis', to: 'Multiple recipients' },
|
||||
{ date: '2023-07-05', type: 'in', amount: 3625, token: 'TEC', usd: 1812, chain: 'gnosis', from: '0x9b55...0b4d' },
|
||||
{ date: '2023-09-10', type: 'out', amount: 3309, token: 'WXDAI', usd: 3309, chain: 'gnosis', to: '0x9b55...0b4d' },
|
||||
{ date: '2023-10-04', type: 'in', amount: 631, token: 'WXDAI', usd: 631, chain: 'gnosis', from: '0x763d...87d4' },
|
||||
{ date: '2023-10-04', type: 'out', amount: 1531, token: 'TEC', usd: 766, chain: 'gnosis', to: '0x763d...87d4' },
|
||||
{ date: '2023-10-14', type: 'in', amount: 9710, token: 'TEC', usd: 4855, chain: 'gnosis', from: '0x5138...Ad00' },
|
||||
{ date: '2023-10-18', type: 'in', amount: 2566, token: 'WXDAI', usd: 2566, chain: 'gnosis', from: '0x9b55...0b4d' },
|
||||
{ date: '2023-10-18', type: 'out', amount: 5900, token: 'TEC', usd: 2950, chain: 'gnosis', to: '0x9b55...0b4d' },
|
||||
{ date: '2023-10-26', type: 'out', amount: 2500, token: 'WXDAI', usd: 2500, chain: 'gnosis', to: '0x9b55...0b4d' },
|
||||
{ date: '2023-11-01', type: 'out', amount: 736, token: 'WXDAI+TEC', usd: 618, chain: 'gnosis', to: 'Multiple recipients' },
|
||||
{ date: '2023-12-15', type: 'out', amount: 5866, token: 'TEC+WXDAI', usd: 3130, chain: 'gnosis', to: '0x7785...3707' },
|
||||
|
||||
// Ethereum
|
||||
{ date: '2024-04-09', type: 'in', amount: 10000, token: 'USDC', usd: 10000, chain: 'ethereum', from: '0xda1A...eE61' },
|
||||
{ date: '2024-04-10', type: 'out', amount: 10000, token: 'USDC', usd: 10000, chain: 'ethereum', to: '0xB90B...6a98' },
|
||||
{ date: '2024-05-08', type: 'in', amount: 10000, token: 'USDC', usd: 10000, chain: 'ethereum', from: '0xda1A...eE61' },
|
||||
{ date: '2024-05-14', type: 'in', amount: 12500, token: 'USDC', usd: 12500, chain: 'ethereum', from: '0xA834...21F4' },
|
||||
{ date: '2024-05-14', type: 'out', amount: 10000, token: 'USDC', usd: 10000, chain: 'ethereum', to: '0xB90B...6a98' },
|
||||
{ date: '2024-06-12', type: 'in', amount: 10000, token: 'USDC', usd: 10000, chain: 'ethereum', from: '0xda1A...eE61' },
|
||||
{ date: '2024-06-12', type: 'out', amount: 10000, token: 'USDC', usd: 10000, chain: 'ethereum', to: '0xB90B...6a98' },
|
||||
{ date: '2024-06-28', type: 'in', amount: 6000, token: 'USDC', usd: 6000, chain: 'ethereum', from: '0xda1A...eE61' },
|
||||
|
||||
// Arbitrum
|
||||
{ date: '2024-10-28', type: 'in', amount: 17, token: 'USDC', usd: 17, chain: 'arbitrum', from: '0x8e1b...a174' },
|
||||
{ date: '2024-10-30', type: 'in', amount: 8, token: 'ARB', usd: 12, chain: 'arbitrum', from: '0x8e1b...a174' },
|
||||
{ date: '2024-10-31', type: 'in', amount: 500, token: 'USDC', usd: 500, chain: 'arbitrum', from: '0x8e1b...a174' },
|
||||
{ date: '2024-11-04', type: 'in', amount: 13, token: 'Mixed', usd: 20, chain: 'arbitrum', from: '0x8e1b...a174' },
|
||||
{ date: '2024-11-06', type: 'in', amount: 5, token: 'Mixed', usd: 8, chain: 'arbitrum', from: '0x8e1b...a174' },
|
||||
{ date: '2024-11-25', type: 'in', amount: 2601, token: 'USDC', usd: 2601, chain: 'arbitrum', from: '0xd2d9...6271' },
|
||||
|
||||
// Avalanche
|
||||
{ date: '2025-03-04', type: 'in', amount: 0.007, token: 'AVAX', usd: 0.25, chain: 'avalanche', from: '0xc13f...' },
|
||||
{ date: '2025-03-10', type: 'in', amount: 0.42, token: 'AVAX', usd: 15, chain: 'avalanche', from: '0xc13f...' },
|
||||
{ date: '2025-03-17', type: 'in', amount: 12500, token: 'USDC', usd: 12500, chain: 'avalanche', from: '0x5129...Cd17' },
|
||||
{ date: '2025-04-17', type: 'out', amount: 7140, token: 'USDC', usd: 7140, chain: 'avalanche', to: 'Multiple recipients' },
|
||||
{ date: '2025-05-07', type: 'in', amount: 0.62, token: 'AVAX', usd: 22, chain: 'avalanche', from: '0xc13f...' },
|
||||
{ date: '2025-05-13', type: 'out', amount: 3220, token: 'USDC+AVAX', usd: 6850, chain: 'avalanche', to: 'Multiple recipients' },
|
||||
|
||||
// Optimism
|
||||
{ date: '2025-02-06', type: 'out', amount: 5740, token: 'DAI+USDC', usd: 5740, chain: 'optimism', to: 'Multiple recipients' },
|
||||
|
||||
// More recent
|
||||
{ date: '2025-10-02', type: 'in', amount: 0.65, token: 'AVAX', usd: 23, chain: 'avalanche', from: '0xc13f...' },
|
||||
{ date: '2025-11-29', type: 'in', amount: 3876, token: 'Yield-USD', usd: 3876, chain: 'ethereum', from: '0x1545...87d4' },
|
||||
{ date: '2025-12-01', type: 'out', amount: 4620, token: 'USDC', usd: 4620, chain: 'ethereum', to: '0x763d...87d4' },
|
||||
{ date: '2025-12-18', type: 'in', amount: 2537, token: 'USDC', usd: 2537, chain: 'avalanche', from: 'CoW Protocol' },
|
||||
{ date: '2025-12-18', type: 'out', amount: 4677, token: 'USDC', usd: 4677, chain: 'avalanche', to: 'Multiple recipients' },
|
||||
{ date: '2025-12-18', type: 'out', amount: 677, token: 'USDC', usd: 677, chain: 'arbitrum', to: '0x09b0...9822' },
|
||||
{ date: '2025-12-19', type: 'out', amount: 6090, token: 'USDC', usd: 6090, chain: 'ethereum', to: '0x0acE...6b87e' },
|
||||
{ date: '2026-01-22', type: 'out', amount: 5000, token: 'USDC', usd: 5000, chain: 'ethereum', to: '0xAbf5...7749' },
|
||||
{ date: '2026-01-22', type: 'out', amount: 5000, token: 'USDC', usd: 5000, chain: 'arbitrum', to: '0x9425...a083' },
|
||||
{ date: '2026-01-29', type: 'in', amount: 0.83, token: 'AVAX', usd: 30, chain: 'avalanche', from: '0x9a9E...' },
|
||||
].map(tx => ({ ...tx, date: new Date(tx.date) })).sort((a, b) => a.date - b.date);
|
||||
|
||||
let currentFilter = { chain: 'all', token: 'all' };
|
||||
let currentZoomTransform = d3.zoomIdentity;
|
||||
|
||||
function filterTransactions() {
|
||||
return transactions.filter(tx => {
|
||||
if (currentFilter.chain !== 'all' && tx.chain !== currentFilter.chain) return false;
|
||||
if (currentFilter.token === 'usdc' && !['USDC', 'DAI', 'WXDAI'].some(t => tx.token.includes(t))) return false;
|
||||
if (currentFilter.token === 'tec' && !tx.token.includes('TEC')) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function calculateStats(txs) {
|
||||
let totalIn = 0, totalOut = 0, balance = 0, peak = 0;
|
||||
|
||||
txs.forEach(tx => {
|
||||
if (tx.type === 'in') {
|
||||
totalIn += tx.usd;
|
||||
balance += tx.usd;
|
||||
} else {
|
||||
totalOut += tx.usd;
|
||||
balance -= tx.usd;
|
||||
}
|
||||
if (balance > peak) peak = balance;
|
||||
});
|
||||
|
||||
document.getElementById('total-inflow').textContent = '$' + Math.round(totalIn).toLocaleString();
|
||||
document.getElementById('total-outflow').textContent = '$' + Math.round(totalOut).toLocaleString();
|
||||
document.getElementById('net-change').textContent = '$' + Math.round(totalIn - totalOut).toLocaleString();
|
||||
document.getElementById('peak-balance').textContent = '$' + Math.round(peak).toLocaleString();
|
||||
document.getElementById('tx-count').textContent = txs.length;
|
||||
}
|
||||
|
||||
// Store references for zoom updates
|
||||
let svgElement, mainGroup, xScale, xAxisGroup, contentGroup;
|
||||
let txs, balanceData, maxBalance, maxTx, balanceScale, flowScale;
|
||||
let margin, width, height, centerY, timeExtent, timePadding;
|
||||
|
||||
function drawTimeline() {
|
||||
const container = document.getElementById('timeline-chart');
|
||||
container.innerHTML = '';
|
||||
|
||||
txs = filterTransactions();
|
||||
calculateStats(txs);
|
||||
|
||||
if (txs.length === 0) {
|
||||
container.innerHTML = '<p style="text-align:center;color:#666;padding:100px;">No transactions match the current filter.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
margin = { top: 100, right: 50, bottom: 80, left: 60 };
|
||||
width = Math.max(1200, container.clientWidth || 1200) - margin.left - margin.right;
|
||||
height = 600 - margin.top - margin.bottom;
|
||||
centerY = height / 2;
|
||||
|
||||
// Create outer SVG
|
||||
svgElement = d3.select('#timeline-chart')
|
||||
.append('svg')
|
||||
.attr('width', width + margin.left + margin.right)
|
||||
.attr('height', height + margin.top + margin.bottom)
|
||||
.style('cursor', 'grab');
|
||||
|
||||
// Main group with margin
|
||||
mainGroup = svgElement.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
// Gradients (in defs, outside clip)
|
||||
const defs = mainGroup.append('defs');
|
||||
|
||||
// Inflow gradient: green at top → cyan/blue at bottom (merging into river)
|
||||
const inflowGradient = defs.append('linearGradient')
|
||||
.attr('id', 'inflowGradient')
|
||||
.attr('x1', '0%').attr('y1', '0%')
|
||||
.attr('x2', '0%').attr('y2', '100%');
|
||||
inflowGradient.append('stop').attr('offset', '0%').attr('stop-color', '#4ade80').attr('stop-opacity', 0.3);
|
||||
inflowGradient.append('stop').attr('offset', '30%').attr('stop-color', '#4ade80').attr('stop-opacity', 0.8);
|
||||
inflowGradient.append('stop').attr('offset', '60%').attr('stop-color', '#22d3ee').attr('stop-opacity', 0.9);
|
||||
inflowGradient.append('stop').attr('offset', '85%').attr('stop-color', '#00d4ff').attr('stop-opacity', 0.95);
|
||||
inflowGradient.append('stop').attr('offset', '100%').attr('stop-color', '#00d4ff').attr('stop-opacity', 1);
|
||||
|
||||
// Outflow gradient: blue at top (leaving river) → red at bottom
|
||||
const outflowGradient = defs.append('linearGradient')
|
||||
.attr('id', 'outflowGradient')
|
||||
.attr('x1', '0%').attr('y1', '0%')
|
||||
.attr('x2', '0%').attr('y2', '100%');
|
||||
outflowGradient.append('stop').attr('offset', '0%').attr('stop-color', '#00d4ff').attr('stop-opacity', 1);
|
||||
outflowGradient.append('stop').attr('offset', '15%').attr('stop-color', '#00d4ff').attr('stop-opacity', 0.95);
|
||||
outflowGradient.append('stop').attr('offset', '40%').attr('stop-color', '#f472b6').attr('stop-opacity', 0.9);
|
||||
outflowGradient.append('stop').attr('offset', '70%').attr('stop-color', '#f87171').attr('stop-opacity', 0.8);
|
||||
outflowGradient.append('stop').attr('offset', '100%').attr('stop-color', '#f87171').attr('stop-opacity', 0.3);
|
||||
|
||||
// River gradient (vertical for depth effect)
|
||||
const riverGradient = defs.append('linearGradient')
|
||||
.attr('id', 'riverGradient')
|
||||
.attr('x1', '0%').attr('y1', '0%')
|
||||
.attr('x2', '0%').attr('y2', '100%');
|
||||
riverGradient.append('stop').attr('offset', '0%').attr('stop-color', '#00d4ff').attr('stop-opacity', 0.9);
|
||||
riverGradient.append('stop').attr('offset', '30%').attr('stop-color', '#0891b2').attr('stop-opacity', 1);
|
||||
riverGradient.append('stop').attr('offset', '50%').attr('stop-color', '#0e7490').attr('stop-opacity', 1);
|
||||
riverGradient.append('stop').attr('offset', '70%').attr('stop-color', '#0891b2').attr('stop-opacity', 1);
|
||||
riverGradient.append('stop').attr('offset', '100%').attr('stop-color', '#00d4ff').attr('stop-opacity', 0.9);
|
||||
|
||||
// Add clip path for content
|
||||
defs.append('clipPath')
|
||||
.attr('id', 'chart-clip')
|
||||
.append('rect')
|
||||
.attr('x', 0)
|
||||
.attr('y', -margin.top)
|
||||
.attr('width', width)
|
||||
.attr('height', height + margin.top + margin.bottom);
|
||||
|
||||
// Time scale (base scale, will be transformed by zoom)
|
||||
timeExtent = d3.extent(txs, d => d.date);
|
||||
timePadding = (timeExtent[1] - timeExtent[0]) * 0.05;
|
||||
xScale = d3.scaleTime()
|
||||
.domain([new Date(timeExtent[0].getTime() - timePadding), new Date(timeExtent[1].getTime() + timePadding)])
|
||||
.range([0, width]);
|
||||
|
||||
// Calculate running balance for river thickness (smooth transitions)
|
||||
balanceData = [];
|
||||
let runningBalance = 0;
|
||||
|
||||
// Add initial point
|
||||
balanceData.push({
|
||||
date: new Date(timeExtent[0].getTime() - timePadding),
|
||||
balance: 0,
|
||||
tx: null
|
||||
});
|
||||
|
||||
txs.forEach(tx => {
|
||||
if (tx.type === 'in') {
|
||||
runningBalance += tx.usd;
|
||||
} else {
|
||||
runningBalance -= tx.usd;
|
||||
}
|
||||
|
||||
balanceData.push({
|
||||
date: tx.date,
|
||||
balance: Math.max(0, runningBalance),
|
||||
tx: tx
|
||||
});
|
||||
});
|
||||
|
||||
// Add final point
|
||||
balanceData.push({
|
||||
date: new Date(timeExtent[1].getTime() + timePadding),
|
||||
balance: Math.max(0, runningBalance),
|
||||
tx: null
|
||||
});
|
||||
|
||||
maxBalance = d3.max(balanceData, d => d.balance) || 1;
|
||||
maxTx = d3.max(txs, d => d.usd) || 1;
|
||||
|
||||
// Scale for river thickness (balance) - make river more prominent
|
||||
// Use linear scale for visual consistency with flows
|
||||
balanceScale = d3.scaleLinear()
|
||||
.domain([0, maxBalance])
|
||||
.range([12, 100]);
|
||||
|
||||
// Scale for flow width - MUST match balanceScale for visual consistency
|
||||
// A $5k flow should create the same width as a $5k change in river
|
||||
flowScale = d3.scaleLinear()
|
||||
.domain([0, maxBalance])
|
||||
.range([0, 100 - 12]); // Match the balanceScale range difference
|
||||
|
||||
// Create content group with clip path (this will be transformed by zoom)
|
||||
contentGroup = mainGroup.append('g')
|
||||
.attr('clip-path', 'url(#chart-clip)')
|
||||
.attr('class', 'content-group');
|
||||
|
||||
// X-axis group (below the clip, updates on zoom)
|
||||
xAxisGroup = mainGroup.append('g')
|
||||
.attr('class', 'x-axis')
|
||||
.attr('transform', `translate(0, ${height + 20})`);
|
||||
|
||||
// Initial axis render
|
||||
updateAxis(xScale);
|
||||
|
||||
// Draw all content with current scale
|
||||
drawContent(xScale);
|
||||
|
||||
// Set up zoom behavior
|
||||
const zoom = d3.zoom()
|
||||
.scaleExtent([0.5, 20]) // Min 0.5x, max 20x zoom
|
||||
.translateExtent([[-width * 2, 0], [width * 3, height]])
|
||||
.extent([[0, 0], [width, height]])
|
||||
.on('zoom', zoomed)
|
||||
.on('start', () => svgElement.style('cursor', 'grabbing'))
|
||||
.on('end', () => svgElement.style('cursor', 'grab'));
|
||||
|
||||
// Custom wheel handler for horizontal scroll panning
|
||||
svgElement.on('wheel', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const currentTransform = currentZoomTransform;
|
||||
|
||||
// Check if it's primarily horizontal scrolling (shift+scroll or trackpad horizontal)
|
||||
if (Math.abs(event.deltaX) > Math.abs(event.deltaY) || event.shiftKey) {
|
||||
// Horizontal scroll = pan
|
||||
const panAmount = event.deltaX !== 0 ? event.deltaX : event.deltaY;
|
||||
const newTransform = currentTransform.translate(-panAmount * 0.5, 0);
|
||||
svgElement.call(zoom.transform, newTransform);
|
||||
} else {
|
||||
// Vertical scroll = zoom (default behavior)
|
||||
const scaleFactor = event.deltaY > 0 ? 0.9 : 1.1;
|
||||
const [mouseX, mouseY] = d3.pointer(event);
|
||||
|
||||
// Zoom centered on mouse position
|
||||
const newScale = Math.max(0.5, Math.min(20, currentTransform.k * scaleFactor));
|
||||
const newX = mouseX - (mouseX - currentTransform.x) * (newScale / currentTransform.k);
|
||||
const newTransform = d3.zoomIdentity.translate(newX, 0).scale(newScale);
|
||||
|
||||
svgElement.call(zoom.transform, newTransform);
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
svgElement.call(zoom);
|
||||
|
||||
// Apply any saved transform
|
||||
if (currentZoomTransform !== d3.zoomIdentity) {
|
||||
svgElement.call(zoom.transform, currentZoomTransform);
|
||||
}
|
||||
|
||||
// Store zoom for reset button
|
||||
window.currentZoom = zoom;
|
||||
|
||||
// Labels (outside clip, static position)
|
||||
mainGroup.append('text')
|
||||
.attr('class', 'flow-label')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', -60)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', '#4ade80')
|
||||
.attr('font-size', '14px')
|
||||
.attr('font-weight', 'bold')
|
||||
.text('INFLOWS');
|
||||
|
||||
mainGroup.append('text')
|
||||
.attr('class', 'flow-label')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', height + 65)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', '#f87171')
|
||||
.attr('font-size', '14px')
|
||||
.attr('font-weight', 'bold')
|
||||
.text('OUTFLOWS');
|
||||
}
|
||||
|
||||
function zoomed(event) {
|
||||
currentZoomTransform = event.transform;
|
||||
const newXScale = event.transform.rescaleX(xScale);
|
||||
|
||||
// Update axis with appropriate tick granularity
|
||||
updateAxis(newXScale);
|
||||
|
||||
// Redraw content with new scale
|
||||
drawContent(newXScale);
|
||||
}
|
||||
|
||||
function updateAxis(scale) {
|
||||
// Determine tick interval based on visible time range
|
||||
const domain = scale.domain();
|
||||
const timeSpan = domain[1] - domain[0];
|
||||
const days = timeSpan / (1000 * 60 * 60 * 24);
|
||||
|
||||
let tickInterval, tickFormat;
|
||||
if (days < 14) {
|
||||
tickInterval = d3.timeDay.every(1);
|
||||
tickFormat = d3.timeFormat('%b %d');
|
||||
} else if (days < 60) {
|
||||
tickInterval = d3.timeWeek.every(1);
|
||||
tickFormat = d3.timeFormat('%b %d');
|
||||
} else if (days < 180) {
|
||||
tickInterval = d3.timeMonth.every(1);
|
||||
tickFormat = d3.timeFormat('%b %Y');
|
||||
} else if (days < 365) {
|
||||
tickInterval = d3.timeMonth.every(2);
|
||||
tickFormat = d3.timeFormat('%b %Y');
|
||||
} else {
|
||||
tickInterval = d3.timeMonth.every(3);
|
||||
tickFormat = d3.timeFormat('%b %Y');
|
||||
}
|
||||
|
||||
const xAxis = d3.axisBottom(scale)
|
||||
.ticks(tickInterval)
|
||||
.tickFormat(tickFormat);
|
||||
|
||||
xAxisGroup.call(xAxis)
|
||||
.selectAll('text')
|
||||
.attr('fill', '#888')
|
||||
.attr('font-size', '11px')
|
||||
.attr('transform', 'rotate(-30)')
|
||||
.attr('text-anchor', 'end');
|
||||
|
||||
xAxisGroup.selectAll('.domain, .tick line')
|
||||
.attr('stroke', '#444');
|
||||
}
|
||||
|
||||
function drawContent(scale) {
|
||||
// Clear previous content
|
||||
contentGroup.selectAll('*').remove();
|
||||
|
||||
// Use smooth curve for organic river shape
|
||||
const smoothCurve = d3.curveBasis;
|
||||
|
||||
// River outer glow/shadow
|
||||
contentGroup.append('path')
|
||||
.datum(balanceData)
|
||||
.attr('fill', 'rgba(0, 212, 255, 0.08)')
|
||||
.attr('d', d3.area()
|
||||
.x(d => scale(d.date))
|
||||
.y0(d => centerY + balanceScale(d.balance) / 2 + 15)
|
||||
.y1(d => centerY - balanceScale(d.balance) / 2 - 15)
|
||||
.curve(smoothCurve)
|
||||
);
|
||||
|
||||
// Main river with smooth organic edges
|
||||
const riverArea = d3.area()
|
||||
.x(d => scale(d.date))
|
||||
.y0(d => centerY + balanceScale(d.balance) / 2)
|
||||
.y1(d => centerY - balanceScale(d.balance) / 2)
|
||||
.curve(smoothCurve);
|
||||
|
||||
contentGroup.append('path')
|
||||
.datum(balanceData)
|
||||
.attr('fill', 'url(#riverGradient)')
|
||||
.attr('d', riverArea);
|
||||
|
||||
// River edge highlights for depth
|
||||
contentGroup.append('path')
|
||||
.datum(balanceData)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', 'rgba(255,255,255,0.3)')
|
||||
.attr('stroke-width', 1.5)
|
||||
.attr('d', d3.line()
|
||||
.x(d => scale(d.date))
|
||||
.y(d => centerY - balanceScale(d.balance) / 2)
|
||||
.curve(smoothCurve)
|
||||
);
|
||||
|
||||
contentGroup.append('path')
|
||||
.datum(balanceData)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', 'rgba(0,0,0,0.2)')
|
||||
.attr('stroke-width', 1)
|
||||
.attr('d', d3.line()
|
||||
.x(d => scale(d.date))
|
||||
.y(d => centerY + balanceScale(d.balance) / 2)
|
||||
.curve(smoothCurve)
|
||||
);
|
||||
|
||||
// Draw flows - SHORT height, focus on WIDTH for proportion
|
||||
// Flows touch the river and match the thickness change they create
|
||||
const flowHeight = 60; // Fixed short height for all flows
|
||||
|
||||
let prevBalance = 0;
|
||||
txs.forEach((tx, i) => {
|
||||
const x = scale(tx.date);
|
||||
// Flow width matches the river thickness change this transaction creates
|
||||
const flowWidth = flowScale(tx.usd);
|
||||
const halfWidth = Math.max(flowWidth / 2, 3); // Minimum 3px half-width for visibility
|
||||
|
||||
// Calculate river thickness BEFORE and AFTER this transaction
|
||||
const balanceBefore = prevBalance;
|
||||
if (tx.type === 'in') {
|
||||
prevBalance += tx.usd;
|
||||
} else {
|
||||
prevBalance -= tx.usd;
|
||||
}
|
||||
const balanceAfter = Math.max(0, prevBalance);
|
||||
const riverTopAfter = centerY - balanceScale(balanceAfter) / 2;
|
||||
const riverBottomAfter = centerY + balanceScale(balanceAfter) / 2;
|
||||
|
||||
if (tx.type === 'in') {
|
||||
// INFLOW: Short flow from above, touching the river top
|
||||
const endY = riverTopAfter; // Flow ends exactly at river edge
|
||||
const startY = endY - flowHeight; // Short fixed height
|
||||
|
||||
// Smooth curved flow using quadratic bezier for organic look
|
||||
const path = d3.path();
|
||||
const midY = startY + flowHeight * 0.5;
|
||||
|
||||
// Left edge curves inward then meets river
|
||||
path.moveTo(x - halfWidth * 0.6, startY);
|
||||
path.quadraticCurveTo(
|
||||
x - halfWidth * 1.1, midY,
|
||||
x - halfWidth, endY
|
||||
);
|
||||
|
||||
// Bottom edge along river
|
||||
path.lineTo(x + halfWidth, endY);
|
||||
|
||||
// Right edge curves back up
|
||||
path.quadraticCurveTo(
|
||||
x + halfWidth * 1.1, midY,
|
||||
x + halfWidth * 0.6, startY
|
||||
);
|
||||
|
||||
path.closePath();
|
||||
|
||||
contentGroup.append('path')
|
||||
.attr('d', path.toString())
|
||||
.attr('fill', 'url(#inflowGradient)')
|
||||
.attr('class', 'flow-path')
|
||||
.attr('opacity', 0.9)
|
||||
.on('mouseover', (event) => showTooltip(event, tx, prevBalance))
|
||||
.on('mousemove', (event) => moveTooltip(event))
|
||||
.on('mouseout', hideTooltip);
|
||||
|
||||
} else {
|
||||
// OUTFLOW: Short flow going below, starting from river bottom
|
||||
const startY = riverBottomAfter; // Flow starts exactly at river edge
|
||||
const endY = startY + flowHeight; // Short fixed height
|
||||
|
||||
// Smooth curved flow
|
||||
const path = d3.path();
|
||||
const midY = startY + flowHeight * 0.5;
|
||||
|
||||
// Left edge from river, curves outward then down
|
||||
path.moveTo(x - halfWidth, startY);
|
||||
path.quadraticCurveTo(
|
||||
x - halfWidth * 1.1, midY,
|
||||
x - halfWidth * 0.6, endY
|
||||
);
|
||||
|
||||
// Bottom edge
|
||||
path.lineTo(x + halfWidth * 0.6, endY);
|
||||
|
||||
// Right edge curves back to river
|
||||
path.quadraticCurveTo(
|
||||
x + halfWidth * 1.1, midY,
|
||||
x + halfWidth, startY
|
||||
);
|
||||
|
||||
path.closePath();
|
||||
|
||||
contentGroup.append('path')
|
||||
.attr('d', path.toString())
|
||||
.attr('fill', 'url(#outflowGradient)')
|
||||
.attr('class', 'flow-path')
|
||||
.attr('opacity', 0.9)
|
||||
.on('mouseover', (event) => showTooltip(event, tx, prevBalance))
|
||||
.on('mousemove', (event) => moveTooltip(event))
|
||||
.on('mouseout', hideTooltip);
|
||||
}
|
||||
});
|
||||
|
||||
// Add invisible overlay for river hover - shows balance at any point
|
||||
const riverHoverGroup = contentGroup.append('g').attr('class', 'river-hover-group');
|
||||
|
||||
// Create invisible wide rectangle over the entire river area for hover detection
|
||||
riverHoverGroup.append('rect')
|
||||
.attr('class', 'river-hover-area')
|
||||
.attr('x', scale.range()[0] - 50)
|
||||
.attr('y', centerY - 100)
|
||||
.attr('width', scale.range()[1] - scale.range()[0] + 100)
|
||||
.attr('height', 200)
|
||||
.attr('fill', 'transparent')
|
||||
.on('mousemove', function(event) {
|
||||
const [mouseX] = d3.pointer(event);
|
||||
const hoveredDate = scale.invert(mouseX);
|
||||
|
||||
// Find the balance at this point in time
|
||||
let balanceAtPoint = 0;
|
||||
let lastTxBeforeHover = null;
|
||||
|
||||
for (let i = 0; i < txs.length; i++) {
|
||||
if (txs[i].date <= hoveredDate) {
|
||||
if (txs[i].type === 'in') {
|
||||
balanceAtPoint += txs[i].usd;
|
||||
} else {
|
||||
balanceAtPoint -= txs[i].usd;
|
||||
}
|
||||
lastTxBeforeHover = txs[i];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Show balance indicator line
|
||||
riverHoverGroup.selectAll('.balance-line').remove();
|
||||
riverHoverGroup.selectAll('.balance-dot').remove();
|
||||
|
||||
const riverThicknessAtPoint = balanceScale(Math.max(0, balanceAtPoint));
|
||||
|
||||
// Vertical indicator line
|
||||
riverHoverGroup.append('line')
|
||||
.attr('class', 'balance-line balance-indicator')
|
||||
.attr('x1', mouseX)
|
||||
.attr('x2', mouseX)
|
||||
.attr('y1', centerY - riverThicknessAtPoint / 2 - 5)
|
||||
.attr('y2', centerY + riverThicknessAtPoint / 2 + 5)
|
||||
.attr('stroke', '#fff')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke-dasharray', '4,2')
|
||||
.attr('opacity', 0.8);
|
||||
|
||||
// Dot at center
|
||||
riverHoverGroup.append('circle')
|
||||
.attr('class', 'balance-dot balance-indicator')
|
||||
.attr('cx', mouseX)
|
||||
.attr('cy', centerY)
|
||||
.attr('r', 5)
|
||||
.attr('fill', '#00d4ff')
|
||||
.attr('stroke', '#fff')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Show tooltip with balance
|
||||
showRiverTooltip(event, hoveredDate, balanceAtPoint);
|
||||
})
|
||||
.on('mouseout', function() {
|
||||
riverHoverGroup.selectAll('.balance-line').remove();
|
||||
riverHoverGroup.selectAll('.balance-dot').remove();
|
||||
hideTooltip();
|
||||
});
|
||||
}
|
||||
|
||||
function showTooltip(event, tx, balanceAfter) {
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
const chainBadge = `<span class="chain-badge ${tx.chain}">${tx.chain.charAt(0).toUpperCase() + tx.chain.slice(1)}</span>`;
|
||||
|
||||
tooltip.innerHTML = `
|
||||
<div class="date">${tx.date.toLocaleDateString('en-US', { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })}</div>
|
||||
${chainBadge}
|
||||
<span class="amount ${tx.type === 'in' ? 'inflow' : 'outflow'}">
|
||||
${tx.type === 'in' ? '+' : '-'}$${tx.usd.toLocaleString()}
|
||||
</span>
|
||||
<div class="token">${tx.amount.toLocaleString()} ${tx.token}</div>
|
||||
<div class="address">${tx.type === 'in' ? 'From: ' + (tx.from || 'Unknown') : 'To: ' + (tx.to || 'Unknown')}</div>
|
||||
<div class="balance-info">Balance after: $${Math.round(Math.max(0, balanceAfter)).toLocaleString()}</div>
|
||||
`;
|
||||
|
||||
tooltip.style.display = 'block';
|
||||
moveTooltip(event);
|
||||
}
|
||||
|
||||
function moveTooltip(event) {
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
const tooltipWidth = tooltip.offsetWidth;
|
||||
const tooltipHeight = tooltip.offsetHeight;
|
||||
|
||||
let x = event.pageX + 15;
|
||||
let y = event.pageY - 10;
|
||||
|
||||
// Keep tooltip on screen
|
||||
if (x + tooltipWidth > window.innerWidth - 20) {
|
||||
x = event.pageX - tooltipWidth - 15;
|
||||
}
|
||||
if (y + tooltipHeight > window.innerHeight - 20) {
|
||||
y = event.pageY - tooltipHeight - 10;
|
||||
}
|
||||
|
||||
tooltip.style.left = x + 'px';
|
||||
tooltip.style.top = y + 'px';
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
document.getElementById('tooltip').style.display = 'none';
|
||||
}
|
||||
|
||||
function showRiverTooltip(event, date, balance) {
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
|
||||
tooltip.innerHTML = `
|
||||
<div class="date">${date.toLocaleDateString('en-US', { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })}</div>
|
||||
<span class="amount balance" style="color: #00d4ff;">
|
||||
💰 $${Math.round(Math.max(0, balance)).toLocaleString()}
|
||||
</span>
|
||||
<div style="margin-top: 8px; font-size: 0.8rem; color: #888;">
|
||||
Wallet balance at this point in time
|
||||
</div>
|
||||
`;
|
||||
|
||||
tooltip.style.display = 'block';
|
||||
moveTooltip(event);
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('chain-filter').addEventListener('change', (e) => {
|
||||
currentFilter.chain = e.target.value;
|
||||
currentZoomTransform = d3.zoomIdentity; // Reset zoom on filter change
|
||||
drawTimeline();
|
||||
});
|
||||
|
||||
document.getElementById('token-filter').addEventListener('change', (e) => {
|
||||
currentFilter.token = e.target.value;
|
||||
currentZoomTransform = d3.zoomIdentity; // Reset zoom on filter change
|
||||
drawTimeline();
|
||||
});
|
||||
|
||||
document.getElementById('reset-zoom').addEventListener('click', () => {
|
||||
currentZoomTransform = d3.zoomIdentity;
|
||||
if (svgElement && window.currentZoom) {
|
||||
svgElement.transition().duration(500).call(window.currentZoom.transform, d3.zoomIdentity);
|
||||
}
|
||||
});
|
||||
|
||||
// Initial draw
|
||||
drawTimeline();
|
||||
|
||||
// Debounced resize handler
|
||||
let resizeTimeout;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => {
|
||||
currentZoomTransform = d3.zoomIdentity;
|
||||
drawTimeline();
|
||||
}, 250);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,548 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Wallet Flow Visualization - 0x2956...7D1</title>
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/d3-sankey@0.12.3/dist/d3-sankey.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
color: #e0e0e0;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
color: #00d4ff;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
margin-bottom: 30px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.stat-card {
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.stat-card h3 {
|
||||
color: #888;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.stat-card .value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.stat-card.inflow .value { color: #4ade80; }
|
||||
.stat-card.outflow .value { color: #f87171; }
|
||||
.stat-card.neutral .value { color: #00d4ff; }
|
||||
|
||||
#sankey-chart {
|
||||
background: rgba(255,255,255,0.02);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.node rect {
|
||||
stroke: #333;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
.node text {
|
||||
font-size: 11px;
|
||||
fill: #e0e0e0;
|
||||
}
|
||||
.link {
|
||||
fill: none;
|
||||
stroke-opacity: 0.4;
|
||||
}
|
||||
.link:hover {
|
||||
stroke-opacity: 0.7;
|
||||
}
|
||||
|
||||
.tables-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
@media (max-width: 1000px) {
|
||||
.tables-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.table-section {
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.table-section h2 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.table-section h2.inflow { color: #4ade80; }
|
||||
.table-section h2.outflow { color: #f87171; }
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 10px 8px;
|
||||
border-bottom: 2px solid rgba(255,255,255,0.1);
|
||||
color: #888;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
td {
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
tr:hover td {
|
||||
background: rgba(255,255,255,0.03);
|
||||
}
|
||||
.address {
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
color: #00d4ff;
|
||||
}
|
||||
.address a {
|
||||
color: #00d4ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.address a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.amount {
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
}
|
||||
.amount.positive { color: #4ade80; }
|
||||
.amount.negative { color: #f87171; }
|
||||
.token {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.token.wxdai { background: #fbbf24; color: #000; }
|
||||
.token.tec { background: #8b5cf6; color: #fff; }
|
||||
.token.zrc { background: #06b6d4; color: #000; }
|
||||
.token.spam { background: #666; color: #999; }
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.legend-color {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: rgba(0,0,0,0.9);
|
||||
color: #fff;
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.spam-warning {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.spam-warning strong { color: #f87171; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Wallet Transaction Flow</h1>
|
||||
<p class="subtitle">gno:0x29567BdBcC92aCF37AC6B56B69180857bB69f7D1</p>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card inflow">
|
||||
<h3>Total Inflow (WXDAI)</h3>
|
||||
<div class="value">+20,197 DAI</div>
|
||||
</div>
|
||||
<div class="stat-card outflow">
|
||||
<h3>Total Outflow (WXDAI)</h3>
|
||||
<div class="value">-17,697 DAI</div>
|
||||
</div>
|
||||
<div class="stat-card inflow">
|
||||
<h3>Total Inflow (TEC)</h3>
|
||||
<div class="value">+14,336 TEC</div>
|
||||
</div>
|
||||
<div class="stat-card outflow">
|
||||
<h3>Total Outflow (TEC)</h3>
|
||||
<div class="value">-13,336 TEC</div>
|
||||
</div>
|
||||
<div class="stat-card neutral">
|
||||
<h3>Unique Counterparties</h3>
|
||||
<div class="value">8 addresses</div>
|
||||
</div>
|
||||
<div class="stat-card neutral">
|
||||
<h3>Active Period</h3>
|
||||
<div class="value">Mar 2023 - Dec 2023</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spam-warning">
|
||||
<strong>⚠️ Note:</strong> This wallet received several spam/scam NFTs from null address (0x000...000) including fake "USDT reward", "ETH Airdrop", and phishing tokens. These are excluded from the legitimate flow analysis below.
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<div class="legend-item"><div class="legend-color" style="background: #fbbf24"></div> WXDAI</div>
|
||||
<div class="legend-item"><div class="legend-color" style="background: #8b5cf6"></div> TEC</div>
|
||||
<div class="legend-item"><div class="legend-color" style="background: #4ade80"></div> Inflow</div>
|
||||
<div class="legend-item"><div class="legend-color" style="background: #f87171"></div> Outflow</div>
|
||||
</div>
|
||||
|
||||
<div id="sankey-chart"></div>
|
||||
|
||||
<div class="tables-grid">
|
||||
<div class="table-section">
|
||||
<h2 class="inflow">↓ Incoming Transfers</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>From</th>
|
||||
<th>Token</th>
|
||||
<th>Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>2023-03-28</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x01d9c9Ca040e90fEB47c7513d9A3574f6e1317bD" target="_blank">0x01d9...17bD</a></td>
|
||||
<td><span class="token wxdai">WXDAI</span></td>
|
||||
<td class="amount positive">+17,000.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-03-22</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x763d7D362B59aeA3858a92a302e18cd41b1252d4" target="_blank">0x763d...87d4</a></td>
|
||||
<td><span class="token tec">TEC</span></td>
|
||||
<td class="amount positive">+1.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-07-05</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x9b55D80Af9dd8D23C372915Ad55c010799010b4d" target="_blank">0x9b55...0b4d</a></td>
|
||||
<td><span class="token tec">TEC</span></td>
|
||||
<td class="amount positive">+3,624.84</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-10-04</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x763d7D362B59aeA3858a92a302e18cd41b1252d4" target="_blank">0x763d...87d4</a></td>
|
||||
<td><span class="token wxdai">WXDAI</span></td>
|
||||
<td class="amount positive">+631.09</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-10-14</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x5138E41b6E66288e273f16380278ffF784ceAd00" target="_blank">0x5138...Ad00</a></td>
|
||||
<td><span class="token tec">TEC</span></td>
|
||||
<td class="amount positive">+9,710.03</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-10-18</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x9b55D80Af9dd8D23C372915Ad55c010799010b4d" target="_blank">0x9b55...0b4d</a></td>
|
||||
<td><span class="token wxdai">WXDAI</span></td>
|
||||
<td class="amount positive">+2,566.40</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2024-05-08</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0xf6A78083ca3e2a662D6dd1703c939c8aCE2e268d" target="_blank">0xf6A7...268d</a></td>
|
||||
<td><span class="token zrc">ZRC</span></td>
|
||||
<td class="amount positive">+500.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2024-05-14</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0xf6A78083ca3e2a662D6dd1703c939c8aCE2e268d" target="_blank">0xf6A7...268d</a></td>
|
||||
<td><span class="token zrc">ZRC</span></td>
|
||||
<td class="amount positive">+500.00</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="table-section">
|
||||
<h2 class="outflow">↑ Outgoing Transfers</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>To</th>
|
||||
<th>Token</th>
|
||||
<th>Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>2023-04-26</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x9b55D80Af9dd8D23C372915Ad55c010799010b4d" target="_blank">0x9b55...0b4d</a></td>
|
||||
<td><span class="token wxdai">WXDAI</span></td>
|
||||
<td class="amount negative">-2,306.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-04-26</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x763d7D362B59aeA3858a92a302e18cd41b1252d4" target="_blank">0x763d...87d4</a></td>
|
||||
<td><span class="token wxdai">WXDAI</span></td>
|
||||
<td class="amount negative">-1,050.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-04-26</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x1409a9ef3450D5d50aAd004f417436e772FbF8fC" target="_blank">0x1409...8fC</a></td>
|
||||
<td><span class="token wxdai">WXDAI</span></td>
|
||||
<td class="amount negative">-910.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-05-11</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0xb2821C0DF0c414ff51D3e8033CBA26DF6AaC587b" target="_blank">0xb282...587b</a></td>
|
||||
<td><span class="token wxdai">WXDAI</span></td>
|
||||
<td class="amount negative">-500.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-06-07</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x9b55D80Af9dd8D23C372915Ad55c010799010b4d" target="_blank">0x9b55...0b4d</a></td>
|
||||
<td><span class="token wxdai">WXDAI</span></td>
|
||||
<td class="amount negative">-3,235.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-06-07</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x763d7D362B59aeA3858a92a302e18cd41b1252d4" target="_blank">0x763d...87d4</a></td>
|
||||
<td><span class="token wxdai">WXDAI</span></td>
|
||||
<td class="amount negative">-2,280.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-06-07</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x778549Eb292AC98A96a05E122967f22eFA003707" target="_blank">0x7785...3707</a></td>
|
||||
<td><span class="token wxdai">WXDAI</span></td>
|
||||
<td class="amount negative">-1,765.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-06-07</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x9239E42792aa0C6881ecFaf73F1ecF0F01C60A14" target="_blank">0x9239...0A14</a></td>
|
||||
<td><span class="token wxdai">WXDAI</span></td>
|
||||
<td class="amount negative">-1,200.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-06-07</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0xb2821C0DF0c414ff51D3e8033CBA26DF6AaC587b" target="_blank">0xb282...587b</a></td>
|
||||
<td><span class="token wxdai">WXDAI</span></td>
|
||||
<td class="amount negative">-445.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-09-10</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x9b55D80Af9dd8D23C372915Ad55c010799010b4d" target="_blank">0x9b55...0b4d</a></td>
|
||||
<td><span class="token wxdai">WXDAI</span></td>
|
||||
<td class="amount negative">-3,309.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-10-04</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x763d7D362B59aeA3858a92a302e18cd41b1252d4" target="_blank">0x763d...87d4</a></td>
|
||||
<td><span class="token tec">TEC</span></td>
|
||||
<td class="amount negative">-1,531.29</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-10-18</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x9b55D80Af9dd8D23C372915Ad55c010799010b4d" target="_blank">0x9b55...0b4d</a></td>
|
||||
<td><span class="token tec">TEC</span></td>
|
||||
<td class="amount negative">-5,900.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-10-26</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x9b55D80Af9dd8D23C372915Ad55c010799010b4d" target="_blank">0x9b55...0b4d</a></td>
|
||||
<td><span class="token wxdai">WXDAI</span></td>
|
||||
<td class="amount negative">-2,500.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-11-01</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0xb2821C0DF0c414ff51D3e8033CBA26DF6AaC587b" target="_blank">0xb282...587b</a></td>
|
||||
<td><span class="token tec">TEC</span></td>
|
||||
<td class="amount negative">-236.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-11-01</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x9239E42792aa0C6881ecFaf73F1ecF0F01C60A14" target="_blank">0x9239...0A14</a></td>
|
||||
<td><span class="token wxdai">WXDAI</span></td>
|
||||
<td class="amount negative">-500.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-12-15</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x778549Eb292AC98A96a05E122967f22eFA003707" target="_blank">0x7785...3707</a></td>
|
||||
<td><span class="token tec">TEC</span></td>
|
||||
<td class="amount negative">-5,668.58</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2023-12-15</td>
|
||||
<td class="address"><a href="https://gnosisscan.io/address/0x778549Eb292AC98A96a05E122967f22eFA003707" target="_blank">0x7785...3707</a></td>
|
||||
<td><span class="token wxdai">WXDAI</span></td>
|
||||
<td class="amount negative">-197.49</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Sankey diagram data
|
||||
const sankeyData = {
|
||||
nodes: [
|
||||
// Inflow sources (left side)
|
||||
{ name: "0x01d9...17bD", type: "source" }, // 0 - large WXDAI source
|
||||
{ name: "0x5138...Ad00", type: "source" }, // 1 - TEC source
|
||||
{ name: "0x9b55...0b4d", type: "source" }, // 2 - bidirectional
|
||||
{ name: "0x763d...87d4", type: "source" }, // 3 - bidirectional
|
||||
{ name: "0xf6A7...268d", type: "source" }, // 4 - ZRC source
|
||||
|
||||
// Central wallet
|
||||
{ name: "Safe Wallet", type: "wallet" }, // 5
|
||||
|
||||
// Outflow targets (right side)
|
||||
{ name: "0x9b55...0b4d (out)", type: "target" }, // 6
|
||||
{ name: "0x763d...87d4 (out)", type: "target" }, // 7
|
||||
{ name: "0x7785...3707", type: "target" }, // 8
|
||||
{ name: "0x9239...0A14", type: "target" }, // 9
|
||||
{ name: "0xb282...587b", type: "target" }, // 10
|
||||
{ name: "0x1409...8fC", type: "target" }, // 11
|
||||
],
|
||||
links: [
|
||||
// Inflows (WXDAI)
|
||||
{ source: 0, target: 5, value: 17000, token: "WXDAI" },
|
||||
{ source: 2, target: 5, value: 2566, token: "WXDAI" },
|
||||
{ source: 3, target: 5, value: 631, token: "WXDAI" },
|
||||
|
||||
// Inflows (TEC)
|
||||
{ source: 1, target: 5, value: 9710, token: "TEC" },
|
||||
{ source: 2, target: 5, value: 3625, token: "TEC" },
|
||||
{ source: 3, target: 5, value: 1, token: "TEC" },
|
||||
|
||||
// Inflows (ZRC)
|
||||
{ source: 4, target: 5, value: 1000, token: "ZRC" },
|
||||
|
||||
// Outflows (WXDAI)
|
||||
{ source: 5, target: 6, value: 11350, token: "WXDAI" },
|
||||
{ source: 5, target: 7, value: 3330, token: "WXDAI" },
|
||||
{ source: 5, target: 8, value: 1962, token: "WXDAI" },
|
||||
{ source: 5, target: 9, value: 1700, token: "WXDAI" },
|
||||
{ source: 5, target: 10, value: 945, token: "WXDAI" },
|
||||
{ source: 5, target: 11, value: 910, token: "WXDAI" },
|
||||
|
||||
// Outflows (TEC)
|
||||
{ source: 5, target: 6, value: 5900, token: "TEC" },
|
||||
{ source: 5, target: 8, value: 5669, token: "TEC" },
|
||||
{ source: 5, target: 7, value: 1531, token: "TEC" },
|
||||
{ source: 5, target: 10, value: 236, token: "TEC" },
|
||||
]
|
||||
};
|
||||
|
||||
const tokenColors = {
|
||||
"WXDAI": "#fbbf24",
|
||||
"TEC": "#8b5cf6",
|
||||
"ZRC": "#06b6d4"
|
||||
};
|
||||
|
||||
// Create Sankey chart
|
||||
const width = 1200;
|
||||
const height = 600;
|
||||
const margin = { top: 20, right: 200, bottom: 20, left: 200 };
|
||||
|
||||
const svg = d3.select("#sankey-chart")
|
||||
.append("svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.attr("viewBox", `0 0 ${width} ${height}`);
|
||||
|
||||
const sankey = d3.sankey()
|
||||
.nodeWidth(20)
|
||||
.nodePadding(15)
|
||||
.extent([[margin.left, margin.top], [width - margin.right, height - margin.bottom]]);
|
||||
|
||||
const { nodes, links } = sankey({
|
||||
nodes: sankeyData.nodes.map(d => Object.assign({}, d)),
|
||||
links: sankeyData.links.map(d => Object.assign({}, d))
|
||||
});
|
||||
|
||||
// Add links
|
||||
svg.append("g")
|
||||
.selectAll("path")
|
||||
.data(links)
|
||||
.join("path")
|
||||
.attr("class", "link")
|
||||
.attr("d", d3.sankeyLinkHorizontal())
|
||||
.attr("stroke", d => tokenColors[d.token] || "#888")
|
||||
.attr("stroke-width", d => Math.max(1, d.width))
|
||||
.append("title")
|
||||
.text(d => `${d.source.name} → ${d.target.name}\n${d.value.toLocaleString()} ${d.token}`);
|
||||
|
||||
// Add nodes
|
||||
const node = svg.append("g")
|
||||
.selectAll("g")
|
||||
.data(nodes)
|
||||
.join("g");
|
||||
|
||||
node.append("rect")
|
||||
.attr("x", d => d.x0)
|
||||
.attr("y", d => d.y0)
|
||||
.attr("height", d => d.y1 - d.y0)
|
||||
.attr("width", d => d.x1 - d.x0)
|
||||
.attr("fill", d => d.type === "wallet" ? "#00d4ff" : d.type === "source" ? "#4ade80" : "#f87171")
|
||||
.attr("rx", 3);
|
||||
|
||||
node.append("text")
|
||||
.attr("x", d => d.x0 < width / 2 ? d.x0 - 6 : d.x1 + 6)
|
||||
.attr("y", d => (d.y1 + d.y0) / 2)
|
||||
.attr("dy", "0.35em")
|
||||
.attr("text-anchor", d => d.x0 < width / 2 ? "end" : "start")
|
||||
.text(d => d.name)
|
||||
.style("font-family", "monospace")
|
||||
.style("font-size", d => d.type === "wallet" ? "14px" : "11px")
|
||||
.style("font-weight", d => d.type === "wallet" ? "bold" : "normal");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue