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:
Jeff Emmett 2026-01-31 13:56:42 +00:00
commit 995a54565a
7 changed files with 2580 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
# Dependencies
node_modules/
# Build outputs
dist/
build/
# OS files
.DS_Store
Thumbs.db
# IDE
.vscode/
.idea/
# Logs
*.log

30
Dockerfile Normal file
View File

@ -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;"]

18
docker-compose.yml Normal file
View File

@ -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

167
index.html Normal file
View File

@ -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>

View File

@ -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>

View File

@ -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>

548
wallet-visualization.html Normal file
View File

@ -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>