Initial commit: Mazacoin blockchain explorer v1.0

- Full-featured blockchain explorer with React frontend + Node.js backend
- Features: block/tx/address lookup, live node map, price ticker, rich list
- Real-time updates via WebSocket
- Comprehensive documentation (README, INSTALL, API, TROUBLESHOOTING)
- Docker deployment with docker-compose
- Deployed at https://maza.samiahmed7777.me
This commit is contained in:
2026-03-09 06:52:46 +01:00
commit 906b1a502d
44 changed files with 5544 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
yarn.lock
# Environment variables
.env
.env.local
.env.development
.env.production
.env.test
*.env
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Build outputs
dist/
build/
*.log
logs/
# Docker
.dockerignore
# SSH keys (NEVER commit these!)
*.key
*.pem
*_rsa
*_rsa.pub
*_ed25519
*_ed25519.pub
id_*
# Database
*.sqlite
*.db
data/
*.sql
# Temporary files
tmp/
temp/
*.tmp
# OS files
Thumbs.db
.DS_Store
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Coverage
coverage/
.nyc_output/
# Test
test-results/
playwright-report/
# Backend data (rich list cache, etc.)
backend/data/*.json
backend/data/*.db
# Frontend production build
frontend/build/
frontend/dist/
# Certificates
*.crt
*.pem
*.cer
# Secrets
secrets/
*.secret
+702
View File
@@ -0,0 +1,702 @@
# Mazacoin Explorer API Documentation
Complete API reference for the Mazacoin blockchain explorer backend.
**Base URL:** `https://maza.samiahmed7777.me/api`
**Protocol:** REST + WebSocket
**Format:** JSON
**Authentication:** None (public read-only API)
---
## Table of Contents
1. [Health & Status](#health--status)
2. [Blockchain Data](#blockchain-data)
3. [Network Information](#network-information)
4. [Search](#search)
5. [Rich List](#rich-list)
6. [WebSocket Events](#websocket-events)
7. [Error Responses](#error-responses)
8. [Rate Limiting](#rate-limiting)
---
## Health & Status
### GET /api/health
Health check endpoint for monitoring.
**Response:**
```json
{
"status": "ok",
"blockHeight": 4128056,
"timestamp": "2026-03-09T05:49:12.000Z"
}
```
**Status Codes:**
- `200 OK` - Service is healthy
- `503 Service Unavailable` - Service is degraded or unavailable
---
## Blockchain Data
### GET /api/blockcount
Get the current blockchain height.
**Response:**
```json
{
"height": 4128056
}
```
**Example:**
```bash
curl https://maza.samiahmed7777.me/api/blockcount
```
---
### GET /api/block/:hashOrHeight
Get detailed information about a specific block.
**Parameters:**
- `hashOrHeight` (string|number) - Block hash or block height
**Response:**
```json
{
"hash": "0000000000000a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5",
"confirmations": 125,
"height": 4128000,
"version": 536870912,
"versionHex": "20000000",
"merkleroot": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2",
"time": 1709964512,
"mediantime": 1709963200,
"nonce": 123456789,
"bits": "1a0fffff",
"difficulty": 1.234567,
"chainwork": "000000000000000000000000000000000000000000000001234567890abcdef",
"nTx": 3,
"previousblockhash": "0000000000000b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b",
"nextblockhash": "0000000000000c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c",
"tx": [
"tx1_hash",
"tx2_hash",
"tx3_hash"
]
}
```
**Example:**
```bash
# By height
curl https://maza.samiahmed7777.me/api/block/4128000
# By hash
curl https://maza.samiahmed7777.me/api/block/0000000000000a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5
```
---
### GET /api/blocks/latest/:count?
Get the latest N blocks with summary information.
**Parameters:**
- `count` (optional, number) - Number of blocks to return (default: 10, max: 100)
**Response:**
```json
{
"blocks": [
{
"height": 4128056,
"hash": "0000000000000...",
"time": 1709964512,
"txCount": 3,
"totalAmount": 152.5,
"size": 1234,
"difficulty": 1.234567
},
{
"height": 4128055,
"hash": "0000000000001...",
"time": 1709964312,
"txCount": 5,
"totalAmount": 328.75,
"size": 2345,
"difficulty": 1.234567
}
],
"count": 2
}
```
**Notes:**
- `totalAmount` excludes coinbase (mining reward) transactions
- Blocks are ordered newest first
**Example:**
```bash
curl https://maza.samiahmed7777.me/api/blocks/latest/20
```
---
### GET /api/tx/:txid
Get detailed information about a transaction.
**Parameters:**
- `txid` (string) - Transaction ID (hash)
**Response:**
```json
{
"txid": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2",
"hash": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2",
"version": 1,
"size": 225,
"vsize": 225,
"locktime": 0,
"vin": [
{
"txid": "prev_tx_hash",
"vout": 0,
"scriptSig": {
"asm": "...",
"hex": "..."
},
"sequence": 4294967295
}
],
"vout": [
{
"value": 50.123456,
"n": 0,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 ... OP_EQUALVERIFY OP_CHECKSIG",
"hex": "76a914...",
"type": "pubkeyhash",
"addresses": [
"MAddress1234567890abcdefghijk"
]
}
},
{
"value": 10.654321,
"n": 1,
"scriptPubKey": {
"asm": "...",
"hex": "...",
"type": "pubkeyhash",
"addresses": [
"MAddress0987654321zyxwvutsr"
]
}
}
],
"hex": "...",
"blockhash": "0000000000000...",
"confirmations": 125,
"time": 1709964512,
"blocktime": 1709964512
}
```
**Example:**
```bash
curl https://maza.samiahmed7777.me/api/tx/1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2
```
---
### GET /api/address/:address/balance
Get balance for a specific address (from rich list cache).
**Parameters:**
- `address` (string) - Mazacoin address
**Response:**
```json
{
"address": "MCU8e7DdJ8D2on5fBQfb4jTn2qX4DGiikw",
"balance": 128183.78143,
"rank": 1,
"lastSeen": 4078045,
"percentOfSupply": 0.0512
}
```
**Notes:**
- Only works for addresses in the top 1000 (rich list)
- Balance is approximate (tracks outputs only, not spent inputs)
- Returns 404 if address not in rich list
**Example:**
```bash
curl https://maza.samiahmed7777.me/api/address/MCU8e7DdJ8D2on5fBQfb4jTn2qX4DGiikw/balance
```
---
## Network Information
### GET /api/stats
Get current network statistics.
**Response:**
```json
{
"height": 4128056,
"difficulty": 1.234567,
"networkhashps": 12345678901234,
"connections": 8,
"version": 1000000,
"subversion": "/Satoshi:0.10.0/",
"protocolversion": 70002,
"timeoffset": 0,
"warnings": ""
}
```
**Example:**
```bash
curl https://maza.samiahmed7777.me/api/stats
```
---
### GET /api/peers
Get list of connected peer nodes (for node map).
**Response:**
```json
{
"peers": [
{
"addr": "123.45.67.89:12835",
"version": 70002,
"subver": "/Satoshi:0.10.0/",
"conntime": 1709960000,
"synced_blocks": 4128056,
"synced_headers": 4128056
}
],
"count": 8
}
```
**Example:**
```bash
curl https://maza.samiahmed7777.me/api/peers
```
---
### GET /api/nodes
Get geolocated peer nodes for the node map (includes geolocation data).
**Response:**
```json
{
"total": 8,
"geolocated": 8,
"nodes": [
{
"ip": "123.45.67.89",
"port": 12835,
"country": "United States",
"countryCode": "US",
"region": "California",
"city": "San Francisco",
"lat": 37.7749,
"lon": -122.4194,
"version": "/Satoshi:0.10.0/",
"conntime": 1709960000
}
],
"countries": {
"US": 2,
"FI": 2,
"NL": 1,
"FR": 1,
"BG": 1,
"SG": 1
}
}
```
**Notes:**
- Includes geolocation for all active peers
- `countries` object shows distribution
- Geolocation happens server-side via ip-api.com
**Example:**
```bash
curl https://maza.samiahmed7777.me/api/nodes
```
---
### GET /api/price
Get current MAZA price in BTC, LTC, and ETH.
**Response:**
```json
{
"MAZA": {
"BTC": 0.00000004,
"LTC": 0.00000123,
"ETH": 0.00000056
},
"timestamp": "2026-03-09T05:49:12.000Z",
"sources": {
"freiexchange": "https://freiexchange.com/market/MAZA/BTC",
"coingecko": "https://www.coingecko.com/en/coins/litecoin"
}
}
```
**Notes:**
- MAZA/BTC from FreiExchange API
- MAZA/LTC and MAZA/ETH calculated from CoinGecko BTC ratios
- Cached for 60 seconds server-side
- No USD price (design decision)
**Example:**
```bash
curl https://maza.samiahmed7777.me/api/price
```
---
## Search
### GET /api/search/:query
Universal search for blocks, transactions, or addresses.
**Parameters:**
- `query` (string) - Search term (block height, block hash, tx hash, or address)
**Response:**
**For block height:**
```json
{
"type": "block",
"result": { /* block object */ }
}
```
**For block hash:**
```json
{
"type": "block",
"result": { /* block object */ }
}
```
**For transaction:**
```json
{
"type": "transaction",
"result": { /* transaction object */ }
}
```
**For address:**
```json
{
"type": "address",
"result": {
"address": "MAddress...",
"balance": 123.45,
"txCount": 10
}
}
```
**Not found:**
```json
{
"type": "unknown",
"error": "Not found"
}
```
**Example:**
```bash
# Search by block height
curl https://maza.samiahmed7777.me/api/search/4128000
# Search by hash
curl https://maza.samiahmed7777.me/api/search/0000000000000a1b2c3d...
# Search by address
curl https://maza.samiahmed7777.me/api/search/MCU8e7DdJ8D2on5fBQfb4jTn2qX4DGiikw
```
---
## Rich List
### GET /api/richlist/:limit?
Get top addresses by balance.
**Parameters:**
- `limit` (optional, number) - Number of addresses to return (default: 100, max: 1000)
**Response:**
```json
{
"addresses": [
{
"rank": 1,
"address": "MCU8e7DdJ8D2on5fBQfb4jTn2qX4DGiikw",
"balance": 128183.78143,
"lastSeen": 4078045,
"percentOfSupply": 0.0512
},
{
"rank": 2,
"address": "MMvvMybGw83fU1quLzCAHVYv7jE1dAm5QV",
"balance": 15000.49,
"lastSeen": 4077832,
"percentOfSupply": 0.0060
}
],
"lastScannedBlock": 4078232,
"totalAddresses": 132,
"isScanning": true,
"blocksBehind": 49824
}
```
**Notes:**
- Balances are approximate (tracks outputs only)
- Scanner runs in background, updating every 5 minutes
- `isScanning: true` means scanner is actively catching up
- `percentOfSupply` calculated based on max supply (2.4 billion MAZA)
**Example:**
```bash
# Get top 10
curl https://maza.samiahmed7777.me/api/richlist/10
# Get top 100 (default)
curl https://maza.samiahmed7777.me/api/richlist
```
---
## WebSocket Events
The explorer uses Socket.IO for real-time updates.
**Connection:**
```javascript
import io from 'socket.io-client';
const socket = io('https://maza.samiahmed7777.me');
socket.on('connect', () => {
console.log('Connected to explorer');
});
```
### Event: `block:new`
Emitted when a new block is mined.
**Payload:**
```json
{
"height": 4128057,
"hash": "0000000000000...",
"time": 1709964712,
"txCount": 3,
"difficulty": 1.234567
}
```
**Example:**
```javascript
socket.on('block:new', (block) => {
console.log('New block:', block.height);
// Update UI with new block
});
```
### Event: `block:height`
Emitted periodically with current blockchain height.
**Payload:**
```json
{
"height": 4128057
}
```
**Example:**
```javascript
socket.on('block:height', (data) => {
console.log('Current height:', data.height);
});
```
### Subscribe to New Blocks
```javascript
socket.emit('subscribe:blocks');
socket.on('block:new', (block) => {
console.log('New block mined:', block);
});
```
---
## Error Responses
All API errors return JSON with the following structure:
```json
{
"error": "Error message",
"code": "ERROR_CODE",
"details": { /* optional additional info */ }
}
```
### Common Error Codes
| HTTP Status | Code | Description |
|-------------|------|-------------|
| 400 | `INVALID_PARAMETER` | Invalid request parameter |
| 404 | `NOT_FOUND` | Block, transaction, or address not found |
| 429 | `RATE_LIMIT_EXCEEDED` | Too many requests |
| 500 | `INTERNAL_ERROR` | Server error |
| 503 | `SERVICE_UNAVAILABLE` | Mazacoin node is unreachable |
**Example Error Response:**
```json
{
"error": "Block not found",
"code": "NOT_FOUND",
"details": {
"query": "999999999"
}
}
```
---
## Rate Limiting
**Current Limits:**
- **General API:** 100 requests per minute per IP
- **Search endpoint:** 30 requests per minute per IP
- **WebSocket connections:** 10 concurrent connections per IP
**Rate Limit Headers:**
```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1709964800
```
**Rate Limit Exceeded Response:**
```json
{
"error": "Rate limit exceeded",
"code": "RATE_LIMIT_EXCEEDED",
"retryAfter": 60
}
```
---
## Best Practices
### Caching
Cache responses client-side to reduce API calls:
- Block data: Cache for 10+ minutes (blocks are immutable after confirmations)
- Network stats: Cache for 30-60 seconds
- Price data: Cache for 60 seconds (already cached server-side)
### Pagination
For large datasets (e.g., transaction history), use pagination:
```
/api/address/:address/txs?page=1&limit=50
```
### Error Handling
Always handle errors gracefully:
```javascript
try {
const response = await fetch('/api/block/4128000');
if (!response.ok) {
const error = await response.json();
console.error('API error:', error.message);
}
const data = await response.json();
} catch (err) {
console.error('Network error:', err);
}
```
### WebSocket Reconnection
Implement reconnection logic for WebSocket:
```javascript
socket.on('disconnect', () => {
console.log('Disconnected, attempting to reconnect...');
setTimeout(() => socket.connect(), 5000);
});
```
---
## Changelog
### v1.0.0 (2026-03-09)
- Initial release
- Block, transaction, and address lookup
- Network statistics
- Live node map with geolocation
- Rich list (top 1000 addresses)
- WebSocket real-time updates
- Price ticker (BTC/LTC/ETH)
---
## Support
For API issues or questions:
- **GitHub Issues:** https://git.dashcaddy.net/sami/mazacoin-explorer/issues
- **Documentation:** See `README.md` and `INSTALL.md`
---
**Last Updated:** March 9, 2026
+603
View File
@@ -0,0 +1,603 @@
# Mazacoin Explorer - Installation Guide
Complete step-by-step instructions for deploying the Mazacoin blockchain explorer.
## Table of Contents
1. [Prerequisites](#prerequisites)
2. [System Requirements](#system-requirements)
3. [Mazacoin Node Setup](#mazacoin-node-setup)
4. [SSH Access Configuration](#ssh-access-configuration)
5. [Explorer Deployment](#explorer-deployment)
6. [DNS & Reverse Proxy Setup](#dns--reverse-proxy-setup)
7. [Verification](#verification)
8. [Troubleshooting](#troubleshooting)
---
## Prerequisites
### Required Services
- **Mazacoin full node** running with RPC enabled
- **Docker** 20.10+ and **docker-compose** 1.29+
- **SSH access** to the Mazacoin node (if remote)
- **Domain name** with DNS configured
- **Reverse proxy** (Caddy recommended) with TLS support
### Network Access
- Mazacoin node must be accessible via SSH (if remote) or local network
- Port 8080 available for the explorer frontend
- Port 3000 available for the explorer backend API
---
## System Requirements
### Minimum Specs
- **CPU:** 2 cores
- **RAM:** 2 GB
- **Disk:** 10 GB free space
- **Network:** Stable internet connection
### Recommended Specs
- **CPU:** 4+ cores
- **RAM:** 4+ GB
- **Disk:** 20+ GB SSD
- **Network:** 100+ Mbps
---
## Mazacoin Node Setup
### Option 1: Windows Node (GUI Wallet)
1. **Download Mazacoin wallet** from https://mazacoin.org/downloads
2. **Install** to desired location (e.g., `E:\coins\MAZA`)
3. **Create RPC configuration file** at `E:\coins\MAZA\maza.conf`:
```ini
# RPC Settings
server=1
rpcuser=mazarpc
rpcpassword=YOUR_SECURE_PASSWORD_HERE
rpcallowip=127.0.0.1
rpcport=12832
# Optional: Enable transaction index (required for full address lookup)
txindex=1
```
4. **Start wallet with RPC enabled:**
```powershell
E:\coins\MAZA\maza-qt.exe -server
```
5. **Verify RPC works:**
```powershell
E:\coins\MAZA\daemon\maza-cli.exe -datadir=E:\coins\MAZA getblockcount
```
### Option 2: Linux Node (Daemon)
1. **Install dependencies:**
```bash
sudo apt update
sudo apt install build-essential libtool autotools-dev automake pkg-config \
libssl-dev libevent-dev bsdmainutils libboost-all-dev
```
2. **Download and compile Mazacoin:**
```bash
git clone https://github.com/MazaCoin/maza.git
cd maza
./autogen.sh
./configure
make
sudo make install
```
3. **Create config file** at `~/.maza/maza.conf`:
```ini
server=1
rpcuser=mazarpc
rpcpassword=YOUR_SECURE_PASSWORD_HERE
rpcallowip=127.0.0.1
rpcport=12832
txindex=1
daemon=1
```
4. **Start daemon:**
```bash
mazacoind
```
5. **Verify:**
```bash
maza-cli getblockcount
```
---
## SSH Access Configuration
### If Mazacoin node is on a remote machine:
1. **Generate SSH key pair** on the explorer server:
```bash
ssh-keygen -t ed25519 -f ~/.ssh/mazacoin_node -C "mazacoin-explorer"
```
2. **Copy public key to node:**
```bash
ssh-copy-id -i ~/.ssh/mazacoin_node.pub user@node-ip
```
3. **Test connection:**
```bash
ssh -i ~/.ssh/mazacoin_node user@node-ip "hostname"
```
4. **Update backend configuration** to use this key (see [Explorer Deployment](#explorer-deployment))
---
## Explorer Deployment
### Step 1: Clone Repository
```bash
git clone https://git.dashcaddy.net/sami/mazacoin-explorer.git
cd mazacoin-explorer
```
### Step 2: Configure Backend
Edit `backend/.env` (create if it doesn't exist):
```env
# Node Environment
NODE_ENV=production
PORT=3000
# Mazacoin Node Connection
MAZA_HOST=100.85.236.10
MAZA_USER=hello
MAZA_CLI_PATH=E:\\coins\\MAZA\\daemon\\maza-cli.exe
MAZA_DATADIR=E:\\coins\\MAZA
SSH_KEY_PATH=/root/.ssh/krystie_to_sami_pc
# API Configuration
CACHE_TTL=60
MAX_BLOCKS_PER_REQUEST=100
WEBSOCKET_POLL_INTERVAL=15000
```
**Important Notes:**
- Use double backslashes `\\` for Windows paths in env files
- Adjust `MAZA_HOST`, `MAZA_USER`, `SSH_KEY_PATH` to match your setup
- If running node locally (not via SSH), set `MAZA_HOST=localhost`
### Step 3: Configure Frontend
Edit `frontend/.env`:
```env
# API URL (leave empty for same-origin, or specify full URL)
REACT_APP_API_URL=
# For development, use:
# REACT_APP_API_URL=http://localhost:3000
```
### Step 4: Build and Start Services
```bash
# Build Docker images
docker-compose build
# Start containers in detached mode
docker-compose up -d
# View logs
docker-compose logs -f
```
### Step 5: Verify Services
```bash
# Check container status
docker-compose ps
# Test backend API
curl http://localhost:3000/api/health
# Expected response:
# {"status":"ok","blockHeight":4128056}
# Test frontend
curl http://localhost:8080
```
---
## DNS & Reverse Proxy Setup
### Option 1: Using DashCaddy (Automated)
If you have DashCaddy installed:
```bash
# Run the deployment script
./deploy.sh
```
This will:
- Create Caddy reverse proxy configuration
- Set up automatic Let's Encrypt TLS
- Configure DNS if using Technitium
### Option 2: Manual Caddy Configuration
Create `/etc/caddy/sites/maza.yourdomain.com`:
```caddy
maza.yourdomain.com {
reverse_proxy localhost:8080
# Optional: API sub-path
handle /api/* {
reverse_proxy localhost:3000
}
# TLS
tls {
protocols tls1.2 tls1.3
}
# Headers
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy no-referrer-when-downgrade
}
# Logging
log {
output file /var/log/caddy/maza.log
}
}
```
Reload Caddy:
```bash
systemctl reload caddy
```
### Option 3: Nginx Configuration
```nginx
server {
listen 80;
server_name maza.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name maza.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/ {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# WebSocket support
location /socket.io/ {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
```
---
## Verification
### 1. Check All Services Running
```bash
# Docker containers
docker ps --filter name=mazacoin
# Expected output:
# mazacoin-backend Up X minutes
# mazacoin-frontend Up X minutes
```
### 2. Test API Endpoints
```bash
# Health check
curl https://maza.yourdomain.com/api/health
# Block count
curl https://maza.yourdomain.com/api/blockcount
# Latest blocks
curl https://maza.yourdomain.com/api/blocks/latest/5
# Network stats
curl https://maza.yourdomain.com/api/stats
```
### 3. Test Frontend
Visit https://maza.yourdomain.com in your browser and verify:
- ✅ Home page loads with latest blocks
- ✅ Search works (try searching a block number)
- ✅ Block detail page displays correctly
- ✅ Node map shows active nodes
- ✅ Network stats are populated
### 4. Test WebSocket
Open browser console on https://maza.yourdomain.com and check:
```
WebSocket connection to 'wss://maza.yourdomain.com/socket.io/' succeeded
```
---
## Troubleshooting
### Backend Won't Start
**Issue:** `Error: connect ECONNREFUSED` or SSH connection fails
**Solutions:**
1. Verify Mazacoin node is running:
```bash
ssh -i ~/.ssh/mazacoin_node user@node-ip "tasklist | findstr maza" # Windows
ssh -i ~/.ssh/mazacoin_node user@node-ip "pgrep -f mazacoin" # Linux
```
2. Test RPC manually:
```bash
ssh -i ~/.ssh/mazacoin_node user@node-ip "maza-cli getblockcount"
```
3. Check backend logs:
```bash
docker logs mazacoin-backend --tail 50
```
### Frontend Shows "API Error"
**Issue:** Frontend can't reach backend API
**Solutions:**
1. Check backend is responding:
```bash
curl http://localhost:3000/api/health
```
2. Verify environment variable `REACT_APP_API_URL` is correct in frontend
3. Check CORS settings in backend if accessing from different domain
4. Review frontend logs:
```bash
docker logs mazacoin-frontend --tail 50
```
### Node Map Not Showing Nodes
**Issue:** Map loads but shows 0 nodes
**Solutions:**
1. Check if Mazacoin node has active peer connections:
```bash
curl http://localhost:3000/api/peers
```
2. Verify IP geolocation API is accessible (rate limits?)
3. Check browser console for JavaScript errors
4. Wait a few minutes for initial geolocation to complete
### SSL Certificate Issues
**Issue:** "Your connection is not private" or certificate errors
**Solutions:**
1. Verify DNS is pointing to correct IP:
```bash
dig maza.yourdomain.com
```
2. Check Caddy logs:
```bash
journalctl -u caddy --no-pager -n 50
```
3. Manually request certificate:
```bash
caddy reload --config /etc/caddy/Caddyfile
```
4. Ensure ports 80 and 443 are open:
```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
```
### High Memory Usage
**Issue:** Backend container using excessive RAM
**Solutions:**
1. Limit Docker container memory:
```yaml
# In docker-compose.yml
services:
backend:
deploy:
resources:
limits:
memory: 512M
```
2. Reduce cache size in backend config
3. Enable MongoDB for persistent caching instead of in-memory cache
### Slow Block Queries
**Issue:** Block detail pages take >5 seconds to load
**Solutions:**
1. Check Mazacoin node responsiveness:
```bash
time ssh user@node "maza-cli getblock BLOCKHASH"
```
2. Enable MongoDB caching (see backend README)
3. Reduce concurrent block fetches (edit `backend/src/rpc/MazacoinRPC.js`)
4. Consider running Mazacoin node on faster hardware
---
## Performance Optimization
### Enable Caching
MongoDB caching can significantly improve performance:
1. Start MongoDB container:
```bash
docker run -d --name mongo \
-p 27017:27017 \
-v /data/mongo:/data/db \
mongo:6
```
2. Update backend `.env`:
```env
MONGO_URI=mongodb://localhost:27017/mazacoin
ENABLE_DB_CACHE=true
```
3. Restart backend:
```bash
docker-compose restart backend
```
### CDN Integration
For faster static asset delivery:
1. Use Cloudflare in front of your domain
2. Enable caching for `/static/*` paths
3. Configure browser caching headers in Caddy
---
## Monitoring
### Health Checks
Set up automated monitoring:
```bash
# Simple uptime check (add to cron)
*/5 * * * * curl -sf https://maza.yourdomain.com/api/health || echo "Explorer down!" | mail -s "Alert" admin@yourdomain.com
```
### Prometheus Metrics (Optional)
The backend can export Prometheus metrics:
1. Enable in backend config
2. Add scrape target to Prometheus:
```yaml
scrape_configs:
- job_name: 'mazacoin-explorer'
static_configs:
- targets: ['localhost:3000']
```
---
## Updating
To update the explorer to a new version:
```bash
cd mazacoin-explorer
# Pull latest code
git pull origin main
# Rebuild images
docker-compose build
# Restart services (zero-downtime)
docker-compose up -d
# Check logs
docker-compose logs -f
```
---
## Backup & Recovery
### Backup Configuration
```bash
# Backup env files
cp backend/.env backend/.env.backup
cp frontend/.env frontend/.env.backup
# Backup Caddy config
sudo cp /etc/caddy/sites/maza.yourdomain.com /etc/caddy/sites/maza.yourdomain.com.backup
```
### Database Backup (if using MongoDB)
```bash
docker exec mongo mongodump --out /backup
```
---
## Support
For issues, questions, or contributions:
- **Issues:** https://git.dashcaddy.net/sami/mazacoin-explorer/issues
- **Docs:** See `README.md` and `TROUBLESHOOTING.md`
- **Mazacoin Community:** https://mazacoin.org
---
**Installation complete! 🎉**
Your Mazacoin blockchain explorer should now be running at your configured domain.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Sami Ahmed
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+281
View File
@@ -0,0 +1,281 @@
# Mazacoin Blockchain Explorer
A full-featured blockchain explorer for Mazacoin (MAZA), featuring real-time block updates, transaction search, network statistics, and a live node map.
🌐 **Live at:** https://maza.samiahmed7777.me
## Features
-**Block Explorer** - Search and view blocks by height or hash
-**Transaction Viewer** - Detailed transaction information with inputs/outputs
-**Address Lookup** - View address details with QR code generation
-**Network Statistics** - Real-time network stats dashboard
-**Live Node Map** - Interactive world map showing geo-located Mazacoin nodes
-**Real-time Updates** - WebSocket integration for live block notifications
-**Search** - Universal search for blocks, transactions, and addresses
-**Dark Mode UI** - Professional blockchain explorer design
## Tech Stack
### Backend
- **Node.js** + Express
- **MongoDB** (planned for caching)
- **Socket.IO** for real-time WebSocket updates
- **SSH** client for Mazacoin RPC connection
- **Node-cache** for query caching
### Frontend
- **React** 18 with React Router
- **TailwindCSS** for styling
- **Leaflet.js** for interactive maps
- **Chart.js** for analytics (ready for expansion)
- **Lucide React** for icons
- **QRCode.react** for address QR codes
### Infrastructure
- **Docker** + docker-compose
- **Nginx** (frontend reverse proxy)
- **Caddy** (main reverse proxy via DashCaddy)
- **Tailscale** network for secure node access
## Prerequisites
1. **Mazacoin Node** running on `100.85.236.10` (Windows PC)
- Wallet path: `E:\coins\MAZA`
- CLI: `E:\coins\MAZA\daemon\maza-cli.exe`
- SSH access configured with key at `~/.ssh/krystie_to_sami_pc`
2. **Docker** and **docker-compose** installed
3. **DashCaddy** running on DNS1 (100.71.97.12)
## Quick Start
### 1. Start the Mazacoin Node
On the Windows PC (100.85.236.10):
```powershell
# Option 1: GUI with RPC enabled
E:\coins\MAZA\maza-qt.exe -server
# Option 2: Check if daemon exists and use it
E:\coins\MAZA\daemon\mazacoind.exe -daemon
```
### 2. Build and Deploy
```bash
cd /root/Projects/mazacoin-explorer
./deploy.sh
```
The deployment script will:
- Build Docker images
- Start frontend and backend containers
- Configure Caddy reverse proxy
- Make the explorer available at https://maza.samiahmed7777.me
### 3. Manual Deployment (Alternative)
```bash
# Build images
docker-compose build
# Start services
docker-compose up -d
# Check logs
docker-compose logs -f
```
## Development
### Backend Development
```bash
cd backend
npm install
npm run dev # Uses nodemon for auto-restart
```
### Frontend Development
```bash
cd frontend
npm install
npm start # Runs on http://localhost:3001
```
**Note:** Update `.env` or `src/config.js` to point to your local backend during development.
## API Endpoints
### Blockchain Data
- `GET /api/health` - Health check
- `GET /api/blockcount` - Latest block height
- `GET /api/block/:hashOrHeight` - Get block by hash or height
- `GET /api/tx/:txid` - Get transaction by ID
- `GET /api/blocks/latest/:count` - Get latest N blocks
- `GET /api/search/:query` - Universal search
### Network Info
- `GET /api/stats` - Network statistics
- `GET /api/peers` - Connected peer nodes (for node map)
### WebSocket Events
- `subscribe:blocks` - Subscribe to new block notifications
- `block:new` - Receive new block events
- `block:height` - Current block height
## Configuration
### Backend Environment Variables
Create `backend/.env`:
```env
NODE_ENV=production
PORT=3000
SSH_KEY_PATH=/root/.ssh/krystie_to_sami_pc
```
### Frontend Environment Variables
Create `frontend/.env`:
```env
REACT_APP_API_URL=https://maza.samiahmed7777.me
```
## Architecture
```
┌─────────────────┐
│ User Browser │
└────────┬────────┘
│ HTTPS
┌─────────────────┐
│ Caddy (DNS1) │ ← DashCaddy managed
│ :443 → :8080 │
└────────┬────────┘
┌─────────────────┐ ┌─────────────────┐
│ Frontend │────▶│ Backend │
│ (React/Nginx) │ │ (Node.js API) │
│ Port 8080 │ │ Port 3000 │
└─────────────────┘ └────────┬────────┘
│ SSH
┌─────────────────┐
│ Mazacoin Node │
│ (sami-pc) │
│ 100.85.236.10 │
└─────────────────┘
```
## Monitoring
### View Logs
```bash
# Backend logs
docker-compose logs -f backend
# Frontend logs
docker-compose logs -f frontend
# All logs
docker-compose logs -f
```
### Check Service Status
```bash
docker-compose ps
```
### Restart Services
```bash
# Restart all
docker-compose restart
# Restart specific service
docker-compose restart backend
docker-compose restart frontend
```
## Troubleshooting
### Backend can't connect to Mazacoin node
1. Check if Mazacoin is running on sami-pc:
```bash
ssh -i ~/.ssh/krystie_to_sami_pc hello@100.85.236.10 "tasklist | findstr maza"
```
2. Test RPC connection:
```bash
ssh -i ~/.ssh/krystie_to_sami_pc hello@100.85.236.10 "E:\coins\MAZA\daemon\maza-cli.exe getblockcount"
```
3. Check backend logs:
```bash
docker-compose logs backend | grep -i error
```
### Frontend shows "API Error"
1. Check if backend is running:
```bash
curl http://localhost:3000/api/health
```
2. Check frontend logs for configuration issues:
```bash
docker-compose logs frontend
```
### Node map not showing nodes
The node map requires:
1. Mazacoin node to have active peer connections
2. Internet access for IP geolocation API calls
3. May take a few minutes to geolocate all peers
## Future Enhancements
- [ ] MongoDB integration for blockchain data caching
- [ ] Full address balance lookup (requires txindex)
- [ ] Transaction mempool viewer
- [ ] Charts for hashrate/difficulty trends
- [ ] Rich list (top addresses by balance)
- [ ] Mining pool statistics
- [ ] Block time analysis
- [ ] API rate limiting
- [ ] Prometheus metrics export
- [ ] Mobile app (React Native)
## Contributing
This is a private project for the Mazacoin community. For questions or suggestions, contact the maintainer.
## License
MIT License - See LICENSE file for details
## Credits
Built with ❤️ for the Mazacoin community
- **Mazacoin:** https://mazacoin.org
- **Explorer:** https://maza.samiahmed7777.me
---
**Deployment Status:** ✅ Ready for deployment
**Last Updated:** March 6, 2026
+188
View File
@@ -0,0 +1,188 @@
# Mazacoin Blockchain Explorer - Requirements
## Project Overview
Build a full-featured blockchain explorer for Mazacoin (similar to blockchain.com or blockchair.com).
**Domain:** maza.samiahmed7777.me
**Mazacoin Node:** Running on Windows PC at 100.85.236.10 (Tailscale), wallet at E:\coins\MAZA
## Core Features
### 1. Block Explorer
- Search by block height or hash
- Display: height, hash, timestamp, size, transactions, miner address
- Previous/next block navigation
- Latest blocks feed (real-time)
### 2. Transaction Viewer
- Search by transaction ID
- Display: inputs, outputs, amounts, fees, confirmations
- Visual input/output flow
- Transaction status (confirmed/pending)
### 3. Address Lookup
- Search by Mazacoin address
- Balance display
- Transaction history (paginated)
- QR code generation for receiving
- Total received/sent
### 4. Network Statistics Dashboard
- Current block height
- Network hashrate
- Mining difficulty
- Average block time
- Total supply
- Active addresses (24h)
- Transactions per day
- Mempool size
### 5. Live Node Map 🌍
- Interactive world map (Leaflet.js)
- Geo-located active Mazacoin nodes
- Real-time updates via WebSocket
- Show: location, IP (optional), version, uptime
- Node count by country
### 6. Charts & Analytics
- Hashrate over time (7d, 30d, all time)
- Transactions per day
- Difficulty adjustment history
- Block time trends
### 7. Rich List
- Top addresses by balance
- Percentage of total supply
- Privacy-friendly (optional anonymization)
### 8. Mining Stats
- Latest mined blocks
- Mining pool distribution (if detectable)
- Average block rewards
## Tech Stack
### Backend
- **Language:** Node.js + Express
- **Database:** MongoDB (for caching blockchain data)
- **Blockchain:** Mazacoin node RPC via `maza-cli.exe`
- **Geolocation:** ipapi.co or ip-api.com
- **Real-time:** WebSocket (Socket.io)
### Frontend
- **Framework:** React (or vanilla JS if simpler)
- **Maps:** Leaflet.js
- **Charts:** Chart.js or Recharts
- **Styling:** TailwindCSS or similar
- **Icons:** Lucide or Font Awesome
### Infrastructure
- **Container:** Docker + docker-compose
- **Deployment:** DashCaddy API (http://100.71.97.12:3001)
- **Reverse Proxy:** Caddy (auto-configured via DashCaddy)
- **Domain:** maza.samiahmed7777.me
## Mazacoin Node Connection
**Node Location:** Windows PC at 100.85.236.10 (SSH: hello@100.85.236.10, key at ~/.ssh/krystie_to_sami_pc)
**Wallet Path:** E:\coins\MAZA
**CLI:** E:\coins\MAZA\daemon\maza-cli.exe
**RPC Commands Available:**
- `getblockcount` - Latest block height
- `getblockhash <height>` - Get block hash
- `getblock <hash>` - Get block details
- `getrawtransaction <txid> 1` - Get transaction details
- `getpeerinfo` - Get connected nodes
- `getnetworkinfo` - Network stats
- `getmininginfo` - Mining difficulty, hashrate
- `getdifficulty` - Current difficulty
**Connection Method:**
1. Start Mazacoin node: `E:\coins\MAZA\maza-qt.exe -server` (enables RPC)
2. Or use daemon: Check if there's a mazacoind.exe in the daemon folder
3. Configure RPC credentials in maza.conf (create if needed)
4. Backend queries via SSH + maza-cli or direct RPC if port exposed
## Project Structure
```
mazacoin-explorer/
├── backend/
│ ├── src/
│ │ ├── api/ # REST endpoints
│ │ ├── rpc/ # Mazacoin RPC client
│ │ ├── db/ # MongoDB models
│ │ ├── workers/ # Background sync workers
│ │ └── websocket/ # Real-time updates
│ ├── Dockerfile
│ └── package.json
├── frontend/
│ ├── public/
│ ├── src/
│ │ ├── components/ # React components
│ │ ├── pages/ # Block, TX, Address pages
│ │ ├── charts/ # Chart components
│ │ └── map/ # Node map
│ ├── Dockerfile
│ └── package.json
├── docker-compose.yml
├── deploy.sh # DashCaddy deployment script
└── README.md
```
## Deployment Steps
1. Build Docker images
2. Push to registry or build on target
3. Deploy via DashCaddy API:
```bash
curl -X POST http://100.71.97.12:3001/api/docker/deploy \
-H "Content-Type: application/json" \
-d '{
"name": "mazacoin-explorer",
"domain": "maza.samiahmed7777.me",
"port": 3000,
"env": {...}
}'
```
## Design Requirements
- **Theme:** Dark mode by default (blockchain explorers are dark)
- **Colors:** Use Mazacoin brand colors if available, or crypto-blue palette
- **Responsive:** Mobile-friendly
- **Performance:** Fast search, lazy loading for large datasets
- **UX:** Clean, intuitive, minimal clicks to info
## Security Considerations
- Rate limiting on API endpoints
- Input validation (block heights, addresses, tx IDs)
- No sensitive RPC credentials exposed
- Optional: Cloudflare proxy for DDoS protection
## Testing
- Unit tests for RPC client
- Integration tests for API endpoints
- E2E tests for critical user flows (search, view block, etc.)
## Timeline
Build this incrementally:
1. **Phase 1:** Backend RPC client + basic API (blocks, txs, addresses)
2. **Phase 2:** Frontend (search, block/tx viewer)
3. **Phase 3:** Network stats dashboard
4. **Phase 4:** Live node map
5. **Phase 5:** Charts + rich list
## Success Criteria
- Can search and view any block, transaction, or address
- Live node map updates in real-time
- Network stats are accurate
- Fast load times (<2s for most pages)
- Deployed and accessible at maza.samiahmed7777.me
---
**Note:** User (Sami) has Mazacoin wallet but node is not currently running. You may need to coordinate with Krystie (me) to start the node or handle RPC connection setup.
When completely finished, run this command to notify me:
openclaw system event --text "Done: Mazacoin explorer built and ready for deployment to maza.samiahmed7777.me" --mode now
+45
View File
@@ -0,0 +1,45 @@
# Mazacoin Explorer - Troubleshooting Log
## 2026-03-07: Whitescreen Issue
### Problem
Explorer showing blank white screen after initial flash of content.
### Root Causes Found
1. **Missing SSH in Docker container**
- Backend container (node:20-alpine) didn't include openssh-client
- Fix: Added `RUN apk add --no-cache openssh-client` to Dockerfile
2. **Sequential SSH calls (30+ second load time)**
- Original code fetched 10 blocks sequentially (~3 sec each = 30+ sec total)
- Fix: Changed to parallel fetching with `Promise.all()`
3. **SSH connection limit exceeded**
- Parallel fetching opened 10 simultaneous SSH connections
- Windows SSH server rejected connections: "(SSH) Channel open failure"
- Fix: Batched parallel requests (3 at a time) to stay under connection limit
### Final Solution
```javascript
// /root/Projects/mazacoin-explorer/backend/src/server.js
// Fetch blocks with limited concurrency (batches of 3)
const BATCH_SIZE = 3;
for (let i = 0; i < heights.length; i += BATCH_SIZE) {
const batch = heights.slice(i, i + BATCH_SIZE);
const batchResults = await Promise.all(batch.map(async (height) => {
// fetch block...
}));
blocks.push(...batchResults.filter(b => b !== null));
}
```
### Current Performance
- API response time: **~0.1 seconds** (was 30-45 seconds)
- No SSH errors
- Backend healthy and responsive
### Next Steps
- Rebuilding frontend container (no-cache) to ensure clean build
- Verify React app loads without JavaScript errors
+20
View File
@@ -0,0 +1,20 @@
FROM node:20-alpine
WORKDIR /app
# Install SSH client (needed for Mazacoin RPC over SSH)
RUN apk add --no-cache openssh-client
# Install dependencies
COPY package*.json ./
RUN npm install --production
# Copy source code
COPY . .
# Create logs directory
RUN mkdir -p logs
EXPOSE 3000
CMD ["node", "src/server.js"]
+27
View File
@@ -0,0 +1,27 @@
{
"name": "mazacoin-explorer-backend",
"version": "1.0.0",
"description": "Mazacoin blockchain explorer backend",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"test": "jest"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"mongoose": "^8.0.0",
"socket.io": "^4.6.0",
"axios": "^1.6.0",
"node-ssh": "^13.1.0",
"node-cache": "^5.1.2",
"dotenv": "^16.3.1",
"winston": "^3.11.0",
"sqlite3": "^5.1.6",
"better-sqlite3": "^9.2.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env node
const Database = require('better-sqlite3');
const path = require('path');
// Node.js v20 has native fetch
const dbPath = path.join(__dirname, '../data/price-history.db');
const db = new Database(dbPath);
// Ensure table exists
db.exec(`
CREATE TABLE IF NOT EXISTS price_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp INTEGER NOT NULL UNIQUE,
price_btc REAL NOT NULL,
price_ltc REAL,
price_eth REAL,
volume_24h REAL,
high_24h REAL,
low_24h REAL,
source TEXT DEFAULT 'freiexchange',
created_at INTEGER DEFAULT (strftime('%s', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_timestamp ON price_history(timestamp);
`);
async function collectPriceData() {
try {
console.log('[' + new Date().toISOString() + '] Fetching MAZA prices...');
// Fetch from FreiExchange
const freiResp = await fetch('https://api.freiexchange.com/public/ticker/MAZA');
const freiData = await freiResp.json();
// Fetch crypto ratios from CoinGecko
const geckoResp = await fetch(
'https://api.coingecko.com/api/v3/simple/price?ids=litecoin,ethereum&vs_currencies=btc',
{ headers: { 'User-Agent': 'Mazacoin Explorer/1.0 (https://maza.samiahmed7777.me)' } }
);
const geckoData = await geckoResp.json();
// Extract MAZA/BTC price from FreiExchange response
const mazaData = freiData.MAZA_BTC?.[0] || freiData.MAZA?.[0] || {};
const mazaBTC = parseFloat(mazaData.last || 0);
const volume24h = parseFloat(mazaData.volume24h_btc || 0);
const high24h = parseFloat(mazaData.high || 0);
const low24h = parseFloat(mazaData.low || 0);
// Calculate MAZA/LTC and MAZA/ETH
const ltcBTC = geckoData.litecoin?.btc || 0;
const ethBTC = geckoData.ethereum?.btc || 0;
const mazaLTC = ltcBTC > 0 ? mazaBTC / ltcBTC : null;
const mazaETH = ethBTC > 0 ? mazaBTC / ethBTC : null;
// Insert into database
const timestamp = Math.floor(Date.now() / 1000);
const stmt = db.prepare(`
INSERT OR REPLACE INTO price_history
(timestamp, price_btc, price_ltc, price_eth, volume_24h, high_24h, low_24h)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(timestamp, mazaBTC, mazaLTC, mazaETH, volume24h, high24h, low24h);
console.log('✅ Price saved:', {
timestamp: new Date(timestamp * 1000).toISOString(),
mazaBTC,
mazaLTC,
mazaETH,
volume24h
});
return { success: true, timestamp, mazaBTC, mazaLTC, mazaETH };
} catch (error) {
console.error('❌ Error collecting price:', error.message);
return { success: false, error: error.message };
} finally {
db.close();
}
}
// Run if called directly
if (require.main === module) {
collectPriceData().then(result => {
process.exit(result.success ? 0 : 1);
});
}
module.exports = collectPriceData;
+45
View File
@@ -0,0 +1,45 @@
const Database = require('better-sqlite3');
const path = require('path');
// Create/connect to database
const dbPath = path.join(__dirname, '../../data/price-history.db');
const db = new Database(dbPath);
// Create price_history table
db.exec(`
CREATE TABLE IF NOT EXISTS price_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp INTEGER NOT NULL UNIQUE,
price_btc REAL NOT NULL,
price_ltc REAL,
price_eth REAL,
volume_24h REAL,
high_24h REAL,
low_24h REAL,
source TEXT DEFAULT 'freiexchange',
created_at INTEGER DEFAULT (strftime('%s', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_timestamp ON price_history(timestamp);
CREATE INDEX IF NOT EXISTS idx_created_at ON price_history(created_at);
`);
console.log('✅ Price history database initialized');
console.log('📍 Database location:', dbPath);
// Test insert
const now = Math.floor(Date.now() / 1000);
const stmt = db.prepare(`
INSERT OR IGNORE INTO price_history
(timestamp, price_btc, price_ltc, price_eth, volume_24h)
VALUES (?, ?, ?, ?, ?)
`);
const testResult = stmt.run(now, 0.00000004, 0.0000025, 0.0000001, 100);
console.log('🧪 Test insert:', testResult.changes > 0 ? 'SUCCESS' : 'SKIPPED (duplicate)');
// Show latest entry
const latest = db.prepare('SELECT * FROM price_history ORDER BY timestamp DESC LIMIT 1').get();
console.log('📊 Latest entry:', latest);
db.close();
+116
View File
@@ -0,0 +1,116 @@
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const fs = require('fs');
const DB_PATH = path.join(__dirname, '../../data/price_history.db');
const INIT_SQL = fs.readFileSync(path.join(__dirname, 'init.sql'), 'utf8');
// Ensure data directory exists
const dataDir = path.dirname(DB_PATH);
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
// Initialize database
const db = new sqlite3.Database(DB_PATH, (err) => {
if (err) {
console.error('Error opening database:', err);
} else {
console.log('Connected to price history database');
// Run init SQL
db.exec(INIT_SQL, (err) => {
if (err) {
console.error('Error initializing database:', err);
} else {
console.log('Price history database initialized');
}
});
}
});
/**
* Log a price entry
*/
function logPrice(priceBtc, priceLtc, priceEth, volume24h, change24h, source = 'live') {
const timestamp = Math.floor(Date.now() / 1000);
return new Promise((resolve, reject) => {
db.run(
`INSERT OR REPLACE INTO price_history (timestamp, price_btc, price_ltc, price_eth, volume_24h, change_24h, source)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[timestamp, priceBtc, priceLtc, priceEth, volume24h, change24h, source],
function(err) {
if (err) reject(err);
else resolve({ id: this.lastID, timestamp });
}
);
});
}
/**
* Get price history for a timeframe
* @param {string} timeframe - '1h', '24h', '7d', '30d', '1y', 'all'
*/
function getHistory(timeframe = '24h') {
const now = Math.floor(Date.now() / 1000);
let since = 0;
switch(timeframe) {
case '1h':
since = now - (60 * 60);
break;
case '24h':
since = now - (24 * 60 * 60);
break;
case '7d':
since = now - (7 * 24 * 60 * 60);
break;
case '30d':
since = now - (30 * 24 * 60 * 60);
break;
case '1y':
since = now - (365 * 24 * 60 * 60);
break;
case 'all':
since = 0;
break;
default:
since = now - (24 * 60 * 60);
}
return new Promise((resolve, reject) => {
db.all(
`SELECT timestamp, price_btc, price_ltc, price_eth, volume_24h, change_24h, source
FROM price_history
WHERE timestamp >= ?
ORDER BY timestamp ASC`,
[since],
(err, rows) => {
if (err) reject(err);
else resolve(rows);
}
);
});
}
/**
* Get latest price entry
*/
function getLatest() {
return new Promise((resolve, reject) => {
db.get(
`SELECT * FROM price_history ORDER BY timestamp DESC LIMIT 1`,
(err, row) => {
if (err) reject(err);
else resolve(row);
}
);
});
}
module.exports = {
db,
logPrice,
getHistory,
getLatest
};
+132
View File
@@ -0,0 +1,132 @@
const fs = require('fs').promises;
const path = require('path');
const HISTORY_FILE = path.join(__dirname, '..', 'data', 'nodes-history.json');
const MAX_AGE_HOURS = 24;
class NodeHistory {
constructor() {
this.nodes = new Map();
this.initialized = false;
}
async init() {
if (this.initialized) return;
try {
// Ensure data directory exists
const dataDir = path.dirname(HISTORY_FILE);
await fs.mkdir(dataDir, { recursive: true });
// Load existing history
try {
const data = await fs.readFile(HISTORY_FILE, 'utf8');
const parsed = JSON.parse(data);
this.nodes = new Map(Object.entries(parsed));
} catch (err) {
// File doesn't exist or is invalid, start fresh
this.nodes = new Map();
}
this.initialized = true;
} catch (err) {
console.error('Error initializing node history:', err);
this.nodes = new Map();
this.initialized = true;
}
}
async updateNode(ip, geoData, isActive = true) {
await this.init();
const now = Date.now();
const existing = this.nodes.get(ip);
if (existing) {
// Update existing node
this.nodes.set(ip, {
...existing,
...geoData,
lastSeen: now,
isActive
});
} else {
// Add new node
this.nodes.set(ip, {
ip,
...geoData,
firstSeen: now,
lastSeen: now,
isActive
});
}
}
async getRecentNodes() {
await this.init();
const cutoff = Date.now() - (MAX_AGE_HOURS * 60 * 60 * 1000);
const recent = [];
// First, mark all as inactive
for (const [ip, node] of this.nodes.entries()) {
if (node.lastSeen >= cutoff) {
recent.push({
...node,
isActive: false // Will be updated to true for currently connected nodes
});
}
}
return recent;
}
async cleanup() {
await this.init();
const cutoff = Date.now() - (MAX_AGE_HOURS * 60 * 60 * 1000);
// Remove nodes older than 24 hours
for (const [ip, node] of this.nodes.entries()) {
if (node.lastSeen < cutoff) {
this.nodes.delete(ip);
}
}
}
async save() {
await this.init();
try {
const obj = Object.fromEntries(this.nodes);
await fs.writeFile(HISTORY_FILE, JSON.stringify(obj, null, 2), 'utf8');
} catch (err) {
console.error('Error saving node history:', err);
}
}
async getStats() {
await this.init();
const cutoff = Date.now() - (MAX_AGE_HOURS * 60 * 60 * 1000);
const recent = Array.from(this.nodes.values()).filter(n => n.lastSeen >= cutoff);
const active = recent.filter(n => n.isActive);
const countries = {};
recent.forEach(node => {
if (node.country) {
countries[node.country] = (countries[node.country] || 0) + 1;
}
});
return {
total: recent.length,
active: active.length,
countries,
nodes: recent
};
}
}
module.exports = new NodeHistory();
+309
View File
@@ -0,0 +1,309 @@
const fs = require('fs').promises;
const path = require('path');
const rpc = require('./rpc/MazacoinRPC');
const DATA_FILE = path.join(__dirname, '../data/rich-list.json');
const STATE_FILE = path.join(__dirname, '../data/rich-list-state.json');
// In-memory cache
let richList = [];
let balanceMap = new Map(); // Full balance map for all addresses during scanning
let txCache = new Map(); // Cache for transaction lookups (LRU, max 10000 entries)
let lastScannedBlock = 0;
let isScanning = false;
// Simple LRU cache management
const MAX_TX_CACHE_SIZE = 10000;
function addToTxCache(txid, tx) {
if (txCache.size >= MAX_TX_CACHE_SIZE) {
// Remove oldest entry (first key)
const firstKey = txCache.keys().next().value;
txCache.delete(firstKey);
}
txCache.set(txid, tx);
}
function getCachedTx(txid) {
return txCache.get(txid);
}
// Load data from disk
async function loadData() {
try {
// Ensure data directory exists
await fs.mkdir(path.dirname(DATA_FILE), { recursive: true });
// Load rich list
try {
const data = await fs.readFile(DATA_FILE, 'utf8');
richList = JSON.parse(data);
// Populate balance map from loaded data
balanceMap.clear();
richList.forEach(item => {
balanceMap.set(item.address, { balance: item.balance, lastSeen: item.lastSeen });
});
} catch (err) {
richList = [];
balanceMap.clear();
}
// Load state
try {
const state = await fs.readFile(STATE_FILE, 'utf8');
const parsed = JSON.parse(state);
lastScannedBlock = parsed.lastScannedBlock || 0;
} catch (err) {
// Start from genesis block (full blockchain scan)
lastScannedBlock = 0;
console.log(`[Rich List] Starting fresh from genesis block 0`);
}
console.log(`[Rich List] Loaded ${richList.length} addresses, last scanned block: ${lastScannedBlock}`);
} catch (error) {
console.error('[Rich List] Error loading data:', error);
}
}
// Save data to disk
async function saveData() {
try {
await fs.mkdir(path.dirname(DATA_FILE), { recursive: true });
// Save rich list (top 1000 to limit file size)
await fs.writeFile(DATA_FILE, JSON.stringify(richList.slice(0, 1000), null, 2));
// Save state (including cache stats)
await fs.writeFile(STATE_FILE, JSON.stringify({
lastScannedBlock,
lastUpdate: new Date().toISOString(),
totalAddresses: balanceMap.size,
txCacheSize: txCache.size
}, null, 2));
// Clear transaction cache if it's getting too large (keep most recent)
if (txCache.size > MAX_TX_CACHE_SIZE * 0.9) {
const entriesToKeep = Array.from(txCache.entries()).slice(-Math.floor(MAX_TX_CACHE_SIZE * 0.5));
txCache.clear();
entriesToKeep.forEach(([key, value]) => txCache.set(key, value));
}
} catch (error) {
console.error('[Rich List] Error saving data:', error);
}
}
// Update balances from a block
async function processBlock(blockHeight) {
try {
const blockHash = await rpc.getBlockHash(blockHeight);
const block = await rpc.getBlock(blockHash);
if (!block || !block.tx) {
return;
}
// Build a map of address => balance change for this block
const balanceChanges = new Map();
// Process all transactions in the block
for (const txid of block.tx) {
const tx = await rpc.getTransaction(txid);
if (!tx) continue;
// Cache current transaction for future lookups
addToTxCache(txid, tx);
// Process inputs (debits) - subtract spent amounts
// Skip coinbase transactions (no inputs)
if (tx.vin && tx.vin.length > 0 && !tx.vin[0].coinbase) {
for (const vin of tx.vin) {
try {
// Look up the previous transaction (check cache first)
let prevTx = getCachedTx(vin.txid);
if (!prevTx) {
prevTx = await rpc.getTransaction(vin.txid);
if (prevTx) {
addToTxCache(vin.txid, prevTx);
}
}
if (prevTx && prevTx.vout && prevTx.vout[vin.vout]) {
const spentOutput = prevTx.vout[vin.vout];
if (spentOutput.scriptPubKey && spentOutput.scriptPubKey.addresses && spentOutput.scriptPubKey.addresses.length > 0) {
const address = spentOutput.scriptPubKey.addresses[0];
const current = balanceChanges.get(address) || 0;
balanceChanges.set(address, current - spentOutput.value);
}
}
} catch (err) {
// Skip if we can't look up the previous transaction
// This might happen for very old transactions or edge cases
}
}
}
// Add outputs (credits to addresses)
if (tx.vout) {
for (const vout of tx.vout) {
if (vout.scriptPubKey && vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.length > 0) {
const address = vout.scriptPubKey.addresses[0];
const current = balanceChanges.get(address) || 0;
balanceChanges.set(address, current + vout.value);
}
}
}
}
// Update balance map with changes
balanceChanges.forEach((change, address) => {
const existing = balanceMap.get(address);
if (existing) {
existing.balance += change;
existing.lastSeen = blockHeight;
} else {
balanceMap.set(address, {
balance: change,
lastSeen: blockHeight
});
}
});
// Remove addresses with zero or negative balances (dust cleanup)
for (const [address, data] of balanceMap.entries()) {
if (data.balance <= 0.00000001) {
balanceMap.delete(address);
}
}
// Update richList from balanceMap (top 1000)
richList = Array.from(balanceMap.entries())
.map(([address, data]) => ({
address,
balance: data.balance,
lastSeen: data.lastSeen
}))
.sort((a, b) => b.balance - a.balance)
.slice(0, 1000);
lastScannedBlock = blockHeight;
} catch (error) {
console.error(`[Rich List] Error processing block ${blockHeight}:`, error.message);
}
}
// Scan blocks incrementally
async function scanBlocks(maxBlocks = 10000) {
if (isScanning) {
return;
}
isScanning = true;
const scanStartTime = Date.now();
try {
const currentHeight = await rpc.getBlockCount();
const startBlock = lastScannedBlock + 1;
const endBlock = Math.min(startBlock + maxBlocks - 1, currentHeight);
if (startBlock > currentHeight) {
console.log('[Rich List] Already up to date');
isScanning = false;
return;
}
const totalBlocks = endBlock - startBlock + 1;
const blocksRemaining = currentHeight - startBlock + 1;
console.log(`[Rich List] Scanning blocks ${startBlock} to ${endBlock}... (${blocksRemaining.toLocaleString()} blocks behind)`);
let lastProgressTime = Date.now();
let blocksProcessed = 0;
for (let height = startBlock; height <= endBlock; height++) {
await processBlock(height);
blocksProcessed++;
// Save progress every 50 blocks
if (height % 50 === 0) {
await saveData();
const elapsed = (Date.now() - scanStartTime) / 1000;
const blocksPerSec = blocksProcessed / elapsed;
const remainingBlocks = currentHeight - height;
const etaSeconds = remainingBlocks / blocksPerSec;
const etaDays = (etaSeconds / 86400).toFixed(1);
const cacheHitRate = txCache.size > 0 ? ((txCache.size / blocksProcessed) * 100).toFixed(1) : '0';
console.log(`[Rich List] Progress: ${height.toLocaleString()}/${currentHeight.toLocaleString()} | ${balanceMap.size} addresses | Top: ${richList[0]?.balance.toFixed(2) || 0} MAZA | Speed: ${blocksPerSec.toFixed(2)} blocks/sec | ETA: ${etaDays} days | TX cache: ${txCache.size}`);
}
}
await saveData();
const elapsed = (Date.now() - scanStartTime) / 1000;
console.log(`[Rich List] Scan complete: ${richList.length} addresses tracked in ${elapsed.toFixed(1)}s`);
} catch (error) {
console.error('[Rich List] Error during scan:', error);
} finally {
isScanning = false;
}
}
// Get top N addresses
function getTopAddresses(limit = 100) {
return {
addresses: richList.slice(0, limit).map((item, index) => ({
rank: index + 1,
address: item.address,
balance: item.balance,
lastSeen: item.lastSeen
})),
lastScannedBlock,
totalAddresses: richList.length,
isScanning
};
}
// Background updater
let updateInterval;
function startBackgroundUpdater(intervalMinutes = 5) {
if (updateInterval) {
clearInterval(updateInterval);
}
updateInterval = setInterval(async () => {
console.log('[Rich List] Starting background scan...');
await scanBlocks(50000);
}, intervalMinutes * 60 * 1000);
console.log(`[Rich List] Background updater started (every ${intervalMinutes} min, 50000 blocks per scan)`);
}
function stopBackgroundUpdater() {
if (updateInterval) {
clearInterval(updateInterval);
updateInterval = null;
}
}
// Initialize
async function initialize() {
await loadData();
// Do an initial scan if we're behind
const currentHeight = await rpc.getBlockCount();
if (currentHeight - lastScannedBlock > 100) {
console.log(`[Rich List] Behind by ${(currentHeight - lastScannedBlock).toLocaleString()} blocks, starting catch-up scan...`);
// Scan in large chunks - will continue in background
scanBlocks(50000); // Non-blocking - continues in background
}
// Start background updater
startBackgroundUpdater(5);
}
module.exports = {
initialize,
getTopAddresses,
scanBlocks,
startBackgroundUpdater,
stopBackgroundUpdater
};
+186
View File
@@ -0,0 +1,186 @@
const { NodeSSH } = require('node-ssh');
const NodeCache = require('node-cache');
const winston = require('winston');
// Logger
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'logs/rpc.log' })
]
});
class MazacoinRPC {
constructor() {
this.ssh = new NodeSSH();
this.cache = new NodeCache({ stdTTL: 30 }); // 30 second cache
this.connected = false;
this.config = {
host: '100.85.236.10',
username: 'hello',
privateKeyPath: process.env.SSH_KEY_PATH || '/root/.ssh/krystie_to_sami_pc'
};
this.cliPath = 'E:\\coins\\MAZA\\daemon\\maza-cli.exe';
}
async connect() {
if (this.connected) {
// Test if connection is still alive
try {
await this.ssh.execCommand('echo test', { options: { timeout: 5000 } });
return; // Connection still good
} catch (error) {
logger.warn('SSH connection test failed, reconnecting...');
this.connected = false;
try {
this.ssh.dispose();
} catch (e) {
// Ignore disposal errors
}
this.ssh = new (require('node-ssh').NodeSSH)();
}
}
try {
await this.ssh.connect({
host: this.config.host,
username: this.config.username,
privateKeyPath: this.config.privateKeyPath,
keepaliveInterval: 10000, // Send keepalive every 10 seconds
keepaliveCountMax: 3
});
this.connected = true;
logger.info('Connected to Mazacoin node via SSH');
} catch (error) {
logger.error('SSH connection failed:', error);
this.connected = false;
throw new Error(`Failed to connect to Mazacoin node: ${error.message}`);
}
}
async execCommand(command, retries = 1) {
await this.connect();
const fullCommand = `powershell.exe -Command "${this.cliPath} -datadir=E:\\coins\\MAZA ${command}"`;
logger.debug(`Executing: ${fullCommand}`);
try {
const result = await this.ssh.execCommand(fullCommand, {
execOptions: { pty: false },
options: { timeout: 30000 } // 30 second timeout
});
if (result.code !== 0) {
throw new Error(`Command failed: ${result.stderr}`);
}
return result.stdout.trim();
} catch (error) {
// If connection error and we have retries left, reconnect and try again
if ((error.code === 'ECONNRESET' || error.message?.includes('Not connected')) && retries > 0) {
logger.warn(`Connection error, retrying... (${retries} attempts left)`);
this.connected = false;
return this.execCommand(command, retries - 1);
}
logger.error(`RPC command failed (${command}):`, error);
throw error;
}
}
async call(method, ...params) {
const cacheKey = `${method}:${params.join(':')}`;
// Check cache
const cached = this.cache.get(cacheKey);
if (cached !== undefined) {
return cached;
}
const paramsStr = params.map(p => `"${p}"`).join(' ');
const command = params.length > 0 ? `${method} ${paramsStr}` : method;
const output = await this.execCommand(command);
let result;
try {
result = JSON.parse(output);
} catch {
result = output; // Return raw if not JSON
}
// Cache the result
this.cache.set(cacheKey, result);
return result;
}
// Blockchain methods
async getBlockCount() {
return parseInt(await this.call('getblockcount'));
}
async getBlockHash(height) {
return await this.call('getblockhash', height);
}
async getBlock(hashOrHeight) {
// If numeric, get hash first
if (typeof hashOrHeight === 'number' || /^\d+$/.test(hashOrHeight)) {
const hash = await this.getBlockHash(parseInt(hashOrHeight));
return await this.call('getblock', hash);
}
return await this.call('getblock', hashOrHeight);
}
async getTransaction(txid) {
return await this.call('getrawtransaction', txid, '1');
}
async getAddressBalance(address) {
// Note: This may require txindex enabled on the node
try {
return await this.call('getaddressbalance', `{"addresses":["${address}"]}`);
} catch (error) {
logger.warn(`getaddressbalance not available, falling back to manual calculation`);
return null;
}
}
async getPeerInfo() {
return await this.call('getpeerinfo');
}
async getNetworkInfo() {
return await this.call('getnetworkinfo');
}
async getMiningInfo() {
return await this.call('getmininginfo');
}
async getDifficulty() {
return parseFloat(await this.call('getdifficulty'));
}
async getConnectionCount() {
return parseInt(await this.call('getconnectioncount'));
}
disconnect() {
if (this.connected) {
this.ssh.dispose();
this.connected = false;
logger.info('Disconnected from Mazacoin node');
}
}
}
module.exports = new MazacoinRPC();
+686
View File
@@ -0,0 +1,686 @@
const express = require('express');
const cors = require('cors');
const http = require('http');
const { Server } = require('socket.io');
const winston = require('winston');
require('dotenv').config();
const rpc = require('./rpc/MazacoinRPC');
const priceHistory = require('./db/priceHistory');
const nodeHistory = require('./nodeHistory');
const richList = require('./richList');
// Logger
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.simple()
),
transports: [
new winston.transports.Console()
]
});
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: "*"
}
});
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json());
// Health check
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Get latest block height
app.get('/api/blockcount', async (req, res) => {
try {
const count = await rpc.getBlockCount();
res.json({ height: count });
} catch (error) {
logger.error('Error getting block count:', error);
res.status(500).json({ error: error.message });
}
});
// Get block by hash or height
app.get('/api/block/:hashOrHeight', async (req, res) => {
try {
const { hashOrHeight } = req.params;
const block = await rpc.getBlock(hashOrHeight);
res.json(block);
} catch (error) {
logger.error(`Error getting block ${req.params.hashOrHeight}:`, error);
res.status(500).json({ error: error.message });
}
});
// Get transaction by txid (with input amounts)
app.get('/api/tx/:txid', async (req, res) => {
try {
const { txid } = req.params;
const tx = await rpc.getTransaction(txid);
// Enrich vin with amounts from previous transactions
if (tx.vin && Array.isArray(tx.vin)) {
const vinWithAmounts = await Promise.all(
tx.vin.map(async (input) => {
// Skip coinbase transactions (no previous tx)
if (input.coinbase) {
return { ...input, value: 0, isCoinbase: true };
}
try {
// Fetch the previous transaction
const prevTx = await rpc.getTransaction(input.txid);
// Get the output being spent (vout index)
const prevOut = prevTx.vout[input.vout];
return {
...input,
value: prevOut?.value || 0,
address: prevOut?.scriptPubKey?.addresses?.[0] || 'Unknown'
};
} catch (error) {
logger.warn(`Could not fetch input amount for ${input.txid}:`, error.message);
return { ...input, value: null, error: 'txindex may not be enabled' };
}
})
);
tx.vin = vinWithAmounts;
// Calculate total input value
tx.totalInput = vinWithAmounts
.filter(v => !v.isCoinbase && v.value !== null)
.reduce((sum, v) => sum + v.value, 0);
}
// Calculate total output value
if (tx.vout && Array.isArray(tx.vout)) {
tx.totalOutput = tx.vout.reduce((sum, v) => sum + (v.value || 0), 0);
}
// Calculate fees (input - output)
if (tx.totalInput !== undefined && tx.totalOutput !== undefined) {
tx.fees = tx.totalInput - tx.totalOutput;
}
res.json(tx);
} catch (error) {
logger.error(`Error getting transaction ${req.params.txid}:`, error);
res.status(500).json({ error: error.message });
}
});
// Get network stats
app.get('/api/stats', async (req, res) => {
try {
const [blockCount, difficulty, networkInfo, miningInfo, peerCount] = await Promise.all([
rpc.getBlockCount(),
rpc.getDifficulty(),
rpc.getNetworkInfo(),
rpc.getMiningInfo(),
rpc.getConnectionCount()
]);
res.json({
blockHeight: blockCount,
difficulty: difficulty,
connections: peerCount,
networkHashrate: miningInfo.networkhashps || 0,
version: networkInfo.version,
protocolVersion: networkInfo.protocolversion,
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error('Error getting stats:', error);
res.status(500).json({ error: error.message });
}
});
// Get peer info (for node map)
app.get('/api/peers', async (req, res) => {
try {
const peers = await rpc.getPeerInfo();
res.json(peers);
} catch (error) {
logger.error('Error getting peers:', error);
res.status(500).json({ error: error.message });
}
});
// Get peers with geolocation data (24-hour history)
app.get('/api/nodes', async (req, res) => {
try {
// Get current connected peers
const peers = await rpc.getPeerInfo();
// Helper to extract IP from addr (handles both IPv4 and IPv6)
const extractIP = (addr) => {
if (!addr) return null;
// IPv6 format: [2001:db8::1]:8333
const ipv6Match = addr.match(/^\[([^\]]+)\]/);
if (ipv6Match) return ipv6Match[1];
// IPv4 format: 192.168.1.1:8333
return addr.split(':')[0];
};
// Helper to check if IP is private/local
const isPrivateIP = (ip) => {
if (!ip) return true;
// IPv4 private ranges
if (ip.startsWith('127.') ||
ip.startsWith('192.168.') ||
ip.startsWith('10.') ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(ip)) {
return true;
}
// IPv6 private/local ranges
const lower = ip.toLowerCase();
if (lower.startsWith('::1') || // localhost
lower.startsWith('fe80:') || // link-local
lower.startsWith('fc00:') || // unique local
lower.startsWith('fd00:')) { // unique local
return true;
}
return false;
};
// Extract unique public IPs from currently connected peers
const currentIPs = new Set(
peers
.map(p => extractIP(p.addr))
.filter(ip => ip && !isPrivateIP(ip))
);
// Cleanup old nodes (older than 24 hours)
await nodeHistory.cleanup();
// Get nodes from history (last 24 hours)
const historicalNodes = await nodeHistory.getRecentNodes();
// Find new IPs that need geolocation
const knownIPs = new Set(historicalNodes.map(n => n.ip));
const newIPs = Array.from(currentIPs).filter(ip => !knownIPs.has(ip));
// Geolocate new IPs
if (newIPs.length > 0) {
const axios = require('axios');
const batchResponse = await axios.post(
'http://ip-api.com/batch?fields=status,country,countryCode,city,lat,lon,query',
newIPs.slice(0, 100).map(ip => ({ query: ip })), // Limit to 100 IPs per batch
{
timeout: 10000,
headers: { 'Content-Type': 'application/json' }
}
);
const geoResults = batchResponse.data;
for (const geo of geoResults) {
if (geo.status === 'success' && geo.lat && geo.lon) {
const isIPv6 = geo.query.includes(':');
await nodeHistory.updateNode(geo.query, {
lat: geo.lat,
lon: geo.lon,
city: geo.city,
country: geo.country,
countryCode: geo.countryCode,
ipVersion: isIPv6 ? 6 : 4
}, true);
}
}
}
// Update lastSeen for currently connected nodes
for (const ip of currentIPs) {
const existing = historicalNodes.find(n => n.ip === ip);
if (existing) {
await nodeHistory.updateNode(ip, {
lat: existing.lat,
lon: existing.lon,
city: existing.city,
country: existing.country,
countryCode: existing.countryCode,
ipVersion: existing.ipVersion
}, true);
}
}
// Mark nodes not currently connected as inactive
for (const node of historicalNodes) {
if (!currentIPs.has(node.ip)) {
await nodeHistory.updateNode(node.ip, {
lat: node.lat,
lon: node.lon,
city: node.city,
country: node.country,
countryCode: node.countryCode,
ipVersion: node.ipVersion
}, false);
}
}
// Save updated history
await nodeHistory.save();
// Get stats (all nodes from last 24 hours)
const stats = await nodeHistory.getStats();
res.json({
total: stats.total,
active: stats.active,
geolocated: stats.nodes.filter(n => n.lat && n.lon).length,
nodes: stats.nodes,
countries: stats.countries
});
} catch (error) {
logger.error('Error getting nodes with geolocation:', error);
res.status(500).json({ error: error.message });
}
});
// Get rich list (top addresses by balance)
app.get('/api/richlist/:limit?', async (req, res) => {
try {
const limit = parseInt(req.params.limit) || 100;
const data = richList.getTopAddresses(Math.min(limit, 1000)); // Max 1000
res.json(data);
} catch (error) {
logger.error('Error getting rich list:', error);
res.status(500).json({ error: error.message });
}
});
// Get address balance from rich list
app.get('/api/address/:address/balance', async (req, res) => {
try {
const { address } = req.params;
const data = richList.getTopAddresses(1000); // Get top 1000
const addressData = data.addresses.find(a => a.address === address);
if (addressData) {
res.json({
address,
balance: addressData.balance,
rank: addressData.rank,
lastSeen: addressData.lastSeen,
source: 'richlist'
});
} else {
res.json({
address,
balance: null,
message: 'Address not found in rich list. It may have low balance or scanner hasn\'t reached this address yet.',
lastScannedBlock: data.lastScannedBlock,
source: 'richlist'
});
}
} catch (error) {
logger.error('Error getting address balance:', error);
res.status(500).json({ error: error.message });
}
});
// Get latest blocks
app.get('/api/blocks/latest/:count?', async (req, res) => {
try {
const count = Math.min(parseInt(req.params.count) || 10, 50);
const currentHeight = await rpc.getBlockCount();
// Fetch blocks in batches of 5 (balance between speed and SSH connection limits)
const heights = Array.from({ length: count }, (_, i) => currentHeight - i).filter(h => h >= 0);
const blocks = [];
const BATCH_SIZE = 5;
for (let i = 0; i < heights.length; i += BATCH_SIZE) {
const batch = heights.slice(i, i + BATCH_SIZE);
const batchResults = await Promise.all(
batch.map(async (height) => {
try {
const block = await rpc.getBlock(height);
return {
height: block.height,
hash: block.hash,
time: block.time,
size: block.size,
txCount: block.tx ? block.tx.length : 0,
totalAmount: 0 // Skipped - would require additional SSH calls per tx
};
} catch (error) {
logger.warn(`Skipping block ${height}:`, error.message);
return null;
}
})
);
blocks.push(...batchResults.filter(b => b !== null));
}
res.json(blocks);
} catch (error) {
logger.error('Error getting latest blocks:', error);
res.status(500).json({ error: error.message });
}
});
// Get MAZA price in BTC, LTC, ETH (cached 60s)
let priceCache = { data: null, ts: 0 };
app.get("/api/price", async (req, res) => {
try {
if (Date.now() - priceCache.ts < 60000 && priceCache.data) {
return res.json(priceCache.data);
}
const https = require("https");
const fetch = (url) => new Promise((resolve, reject) => {
const options = {
headers: {
'User-Agent': 'Mazacoin Explorer/1.0 (https://maza.samiahmed7777.me)'
}
};
https.get(url, options, (r) => {
let d = "";
r.on("data", c => d += c);
r.on("end", () => resolve(JSON.parse(d)));
}).on("error", reject);
});
// Get MAZA/BTC from FreiExchange
const ticker = await fetch("https://api.freiexchange.com/public/ticker/MAZA");
const mazaBtc = ticker.MAZA_BTC ? ticker.MAZA_BTC[0] : null;
const mazaBtcPrice = mazaBtc ? parseFloat(mazaBtc.last) : 0;
// Get BTC/LTC and BTC/ETH ratios from CoinGecko
const cryptoRatios = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=litecoin,ethereum&vs_currencies=btc");
const ltcBtcRatio = cryptoRatios.litecoin ? cryptoRatios.litecoin.btc : 0;
const ethBtcRatio = cryptoRatios.ethereum ? cryptoRatios.ethereum.btc : 0;
// Calculate MAZA/LTC and MAZA/ETH
const mazaLtcPrice = ltcBtcRatio > 0 ? mazaBtcPrice / ltcBtcRatio : 0;
const mazaEthPrice = ethBtcRatio > 0 ? mazaBtcPrice / ethBtcRatio : 0;
const result = {
btc: mazaBtcPrice,
ltc: mazaLtcPrice,
eth: mazaEthPrice,
volume24h: mazaBtc ? parseFloat(mazaBtc.volume24h) : 0,
volume24hBtc: mazaBtc ? parseFloat(mazaBtc.volume24h_btc) : 0,
change24h: mazaBtc ? parseFloat(mazaBtc.percent_change_24h) : 0,
high24h: mazaBtc ? parseFloat(mazaBtc.high) : 0,
low24h: mazaBtc ? parseFloat(mazaBtc.low) : 0,
exchange: "FreiExchange + CoinGecko",
timestamp: new Date().toISOString()
};
// Log price to database (async, don't wait)
priceHistory.logPrice(
mazaBtcPrice,
mazaLtcPrice,
mazaEthPrice,
result.volume24h,
result.change24h,
'live'
).catch(err => logger.error('Error logging price to DB:', err));
priceCache = { data: result, ts: Date.now() };
res.json(result);
} catch (error) {
logger.error("Error fetching price:", error);
if (priceCache.data) return res.json(priceCache.data);
res.status(500).json({ error: error.message });
}
});
// Get price history for charts
app.get('/api/price/history/:timeframe?', async (req, res) => {
try {
const timeframe = req.params.timeframe || '24h';
const history = await priceHistory.getHistory(timeframe);
res.json({
timeframe,
data: history.map(row => ({
timestamp: row.timestamp,
time: new Date(row.timestamp * 1000).toISOString(),
btc: row.price_btc,
ltc: row.price_ltc,
eth: row.price_eth,
volume: row.volume_24h,
change: row.change_24h,
source: row.source
})),
count: history.length
});
} catch (error) {
logger.error('Error getting price history:', error);
res.status(500).json({ error: error.message });
}
});
// Search endpoint
app.get('/api/search/:query', async (req, res) => {
const { query } = req.params;
try {
// Try as block height
if (/^\d+$/.test(query)) {
const height = parseInt(query);
const currentHeight = await rpc.getBlockCount();
if (height <= currentHeight) {
const block = await rpc.getBlock(height);
return res.json({ type: 'block', data: block });
}
}
// Try as block hash (64 hex chars)
if (/^[0-9a-f]{64}$/i.test(query)) {
try {
const block = await rpc.getBlock(query);
return res.json({ type: 'block', data: block });
} catch {}
// Maybe it's a transaction
try {
const tx = await rpc.getTransaction(query);
return res.json({ type: 'transaction', data: tx });
} catch {}
}
// Try as Mazacoin address (starts with M)
if (query.startsWith('M')) {
// For now, return address placeholder
// Full implementation requires txindex
return res.json({
type: 'address',
data: {
address: query,
note: 'Address lookup requires full node with txindex enabled'
}
});
}
res.status(404).json({ error: 'Not found' });
} catch (error) {
logger.error(`Search error for ${query}:`, error);
res.status(500).json({ error: error.message });
}
});
// WebSocket for real-time updates
io.on('connection', (socket) => {
logger.info('Client connected to WebSocket');
socket.on('subscribe:blocks', async () => {
logger.info('Client subscribed to blocks');
// Send current block height
try {
const height = await rpc.getBlockCount();
socket.emit('block:height', { height });
} catch (error) {
logger.error('Error sending initial block height:', error);
}
});
socket.on('disconnect', () => {
logger.info('Client disconnected');
});
});
// Poll for new blocks and emit to clients
let lastBlockHeight = 0;
setInterval(async () => {
try {
const currentHeight = await rpc.getBlockCount();
if (currentHeight > lastBlockHeight) {
logger.info(`New block detected: ${currentHeight}`);
const block = await rpc.getBlock(currentHeight);
io.emit('block:new', {
height: currentHeight,
hash: block.hash,
time: block.time,
txCount: block.tx ? block.tx.length : 0
});
lastBlockHeight = currentHeight;
}
} catch (error) {
logger.error('Error polling for blocks:', error);
}
}, 15000); // Poll every 15 seconds
// Background node discovery - poll peers every 5 minutes to build 24h history
setInterval(async () => {
try {
const axios = require('axios');
const peers = await rpc.getPeerInfo();
const extractIP = (addr) => {
if (!addr) return null;
const ipv6Match = addr.match(/^\[([^\]]+)\]/);
if (ipv6Match) return ipv6Match[1];
return addr.split(':')[0];
};
const isPrivateIP = (ip) => {
if (!ip) return true;
if (ip.startsWith('127.') || ip.startsWith('192.168.') || ip.startsWith('10.') ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(ip)) return true;
const lower = ip.toLowerCase();
if (lower.startsWith('::1') || lower.startsWith('fe80:') ||
lower.startsWith('fc00:') || lower.startsWith('fd00:')) return true;
return false;
};
const currentIPs = new Set(
peers.map(p => extractIP(p.addr)).filter(ip => ip && !isPrivateIP(ip))
);
// Check for new IPs not in history
const recentNodes = await nodeHistory.getRecentNodes();
const knownIPs = new Set(recentNodes.map(n => n.ip));
const newIPs = Array.from(currentIPs).filter(ip => !knownIPs.has(ip));
if (newIPs.length > 0) {
const batchResponse = await axios.post(
'http://ip-api.com/batch?fields=status,country,countryCode,city,lat,lon,query',
newIPs.slice(0, 100).map(ip => ({ query: ip })),
{ timeout: 10000, headers: { 'Content-Type': 'application/json' } }
);
for (const geo of batchResponse.data) {
if (geo.status === 'success' && geo.lat && geo.lon) {
await nodeHistory.updateNode(geo.query, {
lat: geo.lat, lon: geo.lon, city: geo.city,
country: geo.country, countryCode: geo.countryCode,
ipVersion: geo.query.includes(':') ? 6 : 4
}, true);
}
}
}
// Update lastSeen for known active nodes
for (const ip of currentIPs) {
const existing = recentNodes.find(n => n.ip === ip);
if (existing) {
await nodeHistory.updateNode(ip, {
lat: existing.lat, lon: existing.lon, city: existing.city,
country: existing.country, countryCode: existing.countryCode,
ipVersion: existing.ipVersion
}, true);
}
}
// Mark disconnected nodes as inactive
for (const node of recentNodes) {
if (!currentIPs.has(node.ip)) {
await nodeHistory.updateNode(node.ip, {
lat: node.lat, lon: node.lon, city: node.city,
country: node.country, countryCode: node.countryCode,
ipVersion: node.ipVersion
}, false);
}
}
await nodeHistory.cleanup();
await nodeHistory.save();
const stats = await nodeHistory.getStats();
logger.info(`Node discovery: ${stats.total} total (${stats.active} active) across ${Object.keys(stats.countries).length} countries`);
} catch (error) {
logger.error('Error in background node discovery:', error.message);
}
}, 1 * 60 * 1000); // Every 1 minute
// Initialize
async function init() {
// Start server immediately
server.listen(PORT, () => {
logger.info(`Mazacoin Explorer API running on port ${PORT}`);
});
try {
// Test RPC connection
const height = await rpc.getBlockCount();
logger.info(`Connected to Mazacoin node at block height: ${height}`);
lastBlockHeight = height;
// Initialize rich list scanner (in background, don't block)
richList.initialize().catch(err => {
logger.error('Rich list initialization error:', err);
});
} catch (error) {
logger.error('Failed to connect to Mazacoin node:', error);
logger.info('Server will start but RPC calls will fail until node is running');
}
}
init();
// Graceful shutdown
process.on('SIGTERM', () => {
logger.info('SIGTERM received, shutting down gracefully');
server.close(() => {
rpc.disconnect();
process.exit(0);
});
});
Executable
+64
View File
@@ -0,0 +1,64 @@
#!/bin/bash
set -e
echo "🚀 Deploying Mazacoin Explorer to maza.samiahmed7777.me"
# Configuration
DOMAIN="maza.samiahmed7777.me"
DASHCADDY_API="http://100.71.97.12:3001"
PROJECT_DIR="/root/Projects/mazacoin-explorer"
# Build images
echo "📦 Building Docker images..."
cd "$PROJECT_DIR"
docker compose build
# Stop existing containers
echo "🛑 Stopping existing containers..."
docker compose down || true
# Start containers
echo "▶️ Starting containers..."
docker compose up -d
# Wait for services to be healthy
echo "⏳ Waiting for services to start..."
sleep 10
# Check if services are running
if ! docker ps | grep -q mazacoin-frontend; then
echo "❌ Frontend container failed to start"
docker compose logs frontend
exit 1
fi
if ! docker ps | grep -q mazacoin-backend; then
echo "❌ Backend container failed to start"
docker compose logs backend
exit 1
fi
# Configure Caddy reverse proxy via DashCaddy API
echo "🔧 Configuring Caddy reverse proxy..."
curl -X POST "$DASHCADDY_API/api/caddy/routes" \
-H "Content-Type: application/json" \
-d "{
\"domain\": \"$DOMAIN\",
\"upstream\": \"localhost:8080\",
\"type\": \"proxy\"
}" || echo "⚠️ Warning: Failed to configure Caddy (may already be configured)"
echo "✅ Deployment complete!"
echo ""
echo "🌐 Explorer available at: https://$DOMAIN"
echo "📊 API endpoint: https://$DOMAIN/api"
echo ""
echo "📝 To view logs:"
echo " docker compose logs -f backend"
echo " docker compose logs -f frontend"
echo ""
echo "🔄 To restart:"
echo " docker compose restart"
echo ""
echo "🛑 To stop:"
echo " docker compose down"
+37
View File
@@ -0,0 +1,37 @@
version: '3.8'
services:
backend:
build: ./backend
container_name: mazacoin-backend
restart: unless-stopped
ports:
- "3000:3000"
networks:
- mazacoin-net
environment:
- NODE_ENV=production
- PORT=3000
- SSH_KEY_PATH=/root/.ssh/krystie_to_sami_pc
volumes:
- /root/.ssh:/root/.ssh:ro
- backend-logs:/app/logs
- ./backend/data:/app/data
frontend:
build: ./frontend
container_name: mazacoin-frontend
restart: unless-stopped
ports:
- "8081:80"
depends_on:
- backend
networks:
- mazacoin-net
volumes:
backend-logs:
networks:
mazacoin-net:
driver: bridge
+26
View File
@@ -0,0 +1,26 @@
FROM node:20-alpine as build
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm install
# Copy source
COPY . .
# Build app
RUN npm run build
# Production stage
FROM nginx:alpine
# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy built files
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+41
View File
@@ -0,0 +1,41 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Proxy API requests to backend
location /api/ {
proxy_pass http://backend:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 60s;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
}
# Proxy WebSocket connections to backend
location /socket.io/ {
proxy_pass http://backend:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
# React Router support - all other requests go to index.html
location / {
try_files $uri $uri/ /index.html;
}
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 10240;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json;
}
+48
View File
@@ -0,0 +1,48 @@
{
"name": "mazacoin-explorer-frontend",
"version": "1.0.0",
"private": true,
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0",
"axios": "^1.6.0",
"socket.io-client": "^4.6.0",
"react-simple-maps": "^3.0.0",
"chart.js": "^4.4.0",
"react-chartjs-2": "^5.2.0",
"chartjs-adapter-date-fns": "^3.0.0",
"qrcode.react": "^3.1.0",
"date-fns": "^2.30.0",
"lucide-react": "^0.294.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"devDependencies": {
"react-scripts": "5.0.1",
"tailwindcss": "^3.3.5",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.32"
},
"eslintConfig": {
"extends": [
"react-app"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 KiB

+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/png" href="%PUBLIC_URL%/favicon.ico" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#0f172a" />
<meta name="description" content="Mazacoin blockchain explorer - explore blocks, transactions, and addresses" />
<title>Mazacoin Explorer</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 553 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

+53
View File
@@ -0,0 +1,53 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.card {
@apply bg-maza-gray rounded-lg shadow-lg p-6 mb-4;
}
.btn {
@apply px-4 py-2 rounded-lg font-medium transition-colors duration-200;
}
.btn-primary {
@apply bg-maza-blue text-white hover:bg-blue-700;
}
.btn-secondary {
@apply bg-gray-600 text-white hover:bg-gray-700;
}
.input {
@apply w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-gray-100 focus:outline-none focus:border-maza-blue;
}
.stat-card {
@apply bg-gradient-to-br from-maza-gray to-gray-800 rounded-lg p-4 shadow-lg;
}
.hash {
@apply font-mono text-sm break-all;
}
.loading {
@apply flex items-center justify-center p-12;
}
.error {
@apply bg-red-900/30 border border-red-700 text-red-300 px-4 py-3 rounded-lg;
}
+36
View File
@@ -0,0 +1,36 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Header from './components/Header';
import Footer from './components/Footer';
import Home from './pages/Home';
import BlockDetail from './pages/BlockDetail';
import TransactionDetail from './pages/TransactionDetail';
import AddressDetail from './pages/AddressDetail';
import NetworkStats from './pages/NetworkStats';
import NodeMap from './pages/NodeMap';
import RichList from './pages/RichList';
import './App.css';
function App() {
return (
<Router>
<div className="min-h-screen bg-maza-dark text-gray-100 flex flex-col">
<Header />
<main className="flex-grow container mx-auto px-4 py-8">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/block/:hashOrHeight" element={<BlockDetail />} />
<Route path="/tx/:txid" element={<TransactionDetail />} />
<Route path="/address/:address" element={<AddressDetail />} />
<Route path="/stats" element={<NetworkStats />} />
<Route path="/nodes" element={<NodeMap />} />
<Route path="/richlist" element={<RichList />} />
</Routes>
</main>
<Footer />
</div>
</Router>
);
}
export default App;
+28
View File
@@ -0,0 +1,28 @@
import React from 'react';
function Footer() {
return (
<footer className="bg-maza-gray border-t border-gray-700 py-6 mt-12">
<div className="container mx-auto px-4">
<div className="text-center text-gray-400 mb-4">
<p>Mazacoin Explorer &copy; 2026 | Built for the Mazacoin community</p>
<p className="text-sm mt-2">
<a href="https://mazacoin.org" target="_blank" rel="noopener noreferrer" className="hover:text-maza-blue">
mazacoin.org
</a>
</p>
</div>
<div className="flex items-center justify-center gap-3 text-gray-400 text-sm">
<span>Provided by</span>
<img
src="/samiahmed77777-logo.png"
alt="samiahmed77777"
className="h-8"
/>
</div>
</div>
</footer>
);
}
export default Footer;
+38
View File
@@ -0,0 +1,38 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Activity, MapPin, TrendingUp } from 'lucide-react';
function Header() {
return (
<header className="bg-maza-gray shadow-lg border-b border-gray-700">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<Link to="/" className="flex items-center space-x-3">
<img src="/maza-logo.png" alt="Mazacoin" className="w-8 h-8" />
<span className="text-2xl font-bold">Mazacoin Explorer</span>
</Link>
<nav className="flex items-center space-x-6">
<Link to="/" className="hover:text-maza-blue transition-colors">
Home
</Link>
<Link to="/stats" className="hover:text-maza-blue transition-colors flex items-center gap-2">
<Activity className="w-4 h-4" />
Network Stats
</Link>
<Link to="/nodes" className="hover:text-maza-blue transition-colors flex items-center gap-2">
<MapPin className="w-4 h-4" />
Node Map
</Link>
<Link to="/richlist" className="hover:text-maza-blue transition-colors flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
Rich List
</Link>
</nav>
</div>
</div>
</header>
);
}
export default Header;
+212
View File
@@ -0,0 +1,212 @@
import React, { useState, useEffect } from 'react';
import { Line } from 'react-chartjs-2';
import { Chart as ChartJS, CategoryScale, LinearScale, LogarithmicScale, PointElement, LineElement, Title, Tooltip, Legend, TimeScale } from 'chart.js';
import 'chartjs-adapter-date-fns';
import axios from 'axios';
ChartJS.register(CategoryScale, LinearScale, LogarithmicScale, PointElement, LineElement, Title, Tooltip, Legend, TimeScale);
const API_URL = process.env.REACT_APP_API_URL || '';
function PriceChart({ currency = 'btc', timeframe = '30d' }) {
const [chartData, setChartData] = useState(null);
const [loading, setLoading] = useState(true);
const [activeTimeframe, setActiveTimeframe] = useState(timeframe);
const [activeCurrency, setActiveCurrency] = useState(currency);
useEffect(() => {
fetchChartData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTimeframe, activeCurrency]);
const fetchChartData = async () => {
try {
setLoading(true);
const response = await axios.get(`${API_URL}/api/price/history/${activeTimeframe}`);
const data = response.data.data;
if (!data || data.length === 0) {
setChartData(null);
setLoading(false);
return;
}
const labels = data.map(d => new Date(d.timestamp * 1000));
const prices = data.map(d => d[activeCurrency]);
setChartData({
labels,
datasets: [
{
label: `MAZA/${activeCurrency.toUpperCase()} Price`,
data: prices,
borderColor: activeCurrency === 'btc' ? '#f59e0b' : activeCurrency === 'ltc' ? '#a855f7' : '#3b82f6',
backgroundColor: activeCurrency === 'btc' ? '#f59e0b33' : activeCurrency === 'ltc' ? '#a855f733' : '#3b82f633',
tension: 0.4,
fill: true,
pointRadius: data.length < 50 ? 4 : 2,
pointHoverRadius: 6
}
]
});
} catch (error) {
console.error('Error fetching chart data:', error);
} finally {
setLoading(false);
}
};
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
labels: {
color: '#9ca3af'
}
},
title: {
display: false
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += context.parsed.y.toFixed(activeCurrency === 'eth' ? 10 : 8);
}
return label;
}
}
}
},
scales: {
x: {
type: 'time',
time: {
unit: activeTimeframe === '24h' ? 'hour' : activeTimeframe === '7d' ? 'day' : 'month',
tooltipFormat: 'PPpp',
displayFormats: {
hour: 'MMM d, HH:mm',
day: 'MMM d',
month: 'MMM yyyy'
}
},
grid: {
color: '#1f2937'
},
ticks: {
color: '#9ca3af'
}
},
y: {
type: 'logarithmic',
grid: {
color: '#1f2937'
},
ticks: {
color: '#9ca3af',
callback: function(value) {
return value.toFixed(activeCurrency === 'eth' ? 10 : 8);
}
}
}
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
}
};
const timeframes = [
{ value: '24h', label: '24H' },
{ value: '7d', label: '7D' },
{ value: '30d', label: '30D' },
{ value: '1y', label: '1Y' },
{ value: 'all', label: 'ALL' }
];
const currencies = [
{ value: 'btc', label: 'BTC', color: 'text-yellow-400' },
{ value: 'ltc', label: 'LTC', color: 'text-purple-400' },
{ value: 'eth', label: 'ETH', color: 'text-blue-400' }
];
if (loading) {
return (
<div className="card">
<div className="loading">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
</div>
</div>
);
}
if (!chartData) {
return (
<div className="card">
<p className="text-gray-400 text-center py-8">Building price history... Check back soon!</p>
</div>
);
}
return (
<div className="card">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold">Price Chart</h2>
<div className="flex gap-4">
{/* Currency selector */}
<div className="flex gap-2">
{currencies.map(curr => (
<button
key={curr.value}
onClick={() => setActiveCurrency(curr.value)}
className={`px-3 py-1 rounded-lg font-medium transition-colors ${
activeCurrency === curr.value
? `${curr.color} bg-gray-700`
: 'text-gray-400 hover:text-gray-300'
}`}
>
{curr.label}
</button>
))}
</div>
{/* Timeframe selector */}
<div className="flex gap-2">
{timeframes.map(tf => (
<button
key={tf.value}
onClick={() => setActiveTimeframe(tf.value)}
className={`px-3 py-1 rounded-lg font-medium transition-colors ${
activeTimeframe === tf.value
? 'bg-maza-blue text-white'
: 'text-gray-400 hover:text-gray-300'
}`}
>
{tf.label}
</button>
))}
</div>
</div>
</div>
<div className="h-96">
<Line data={chartData} options={options} />
</div>
<div className="mt-4 text-sm text-gray-500 text-center">
Historical data from FreiExchange + CoinGecko. Data collection started March 2026.
</div>
</div>
);
}
export default PriceChart;
+72
View File
@@ -0,0 +1,72 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Search } from 'lucide-react';
import axios from 'axios';
const API_URL = process.env.REACT_APP_API_URL || '';
function SearchBar() {
const [query, setQuery] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const navigate = useNavigate();
const handleSearch = async (e) => {
e.preventDefault();
if (!query.trim()) return;
setLoading(true);
setError('');
try {
const response = await axios.get(`${API_URL}/api/search/${query.trim()}`);
const { type, data } = response.data;
if (type === 'block') {
navigate(`/block/${data.hash}`);
} else if (type === 'transaction') {
navigate(`/tx/${data.txid}`);
} else if (type === 'address') {
navigate(`/address/${data.address}`);
}
} catch (err) {
setError(err.response?.data?.error || 'Not found. Try a block height, block hash, transaction ID, or address.');
} finally {
setLoading(false);
}
};
return (
<div className="w-full max-w-3xl mx-auto">
<form onSubmit={handleSearch} className="relative">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search by block height, hash, transaction ID, or address..."
className="input pr-12"
disabled={loading}
/>
<button
type="submit"
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-maza-blue transition-colors"
disabled={loading}
>
{loading ? (
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-maza-blue"></div>
) : (
<Search className="w-6 h-6" />
)}
</button>
</form>
{error && (
<div className="error mt-4">
{error}
</div>
)}
</div>
);
}
export default SearchBar;
+11
View File
@@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './App.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
+131
View File
@@ -0,0 +1,131 @@
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { QRCodeSVG } from 'qrcode.react';
import { Copy, TrendingUp } from 'lucide-react';
import axios from 'axios';
const API_URL = process.env.REACT_APP_API_URL || '';
function AddressDetail() {
const { address } = useParams();
const [balanceData, setBalanceData] = useState(null);
const [loadingBalance, setLoadingBalance] = useState(true);
useEffect(() => {
fetchBalance();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [address]);
const fetchBalance = async () => {
try {
const response = await axios.get(`${API_URL}/api/address/${address}/balance`);
setBalanceData(response.data);
} catch (error) {
console.error('Error fetching balance:', error);
} finally {
setLoadingBalance(false);
}
};
const copyToClipboard = () => {
navigator.clipboard.writeText(address);
};
const formatBalance = (balance) => {
return balance.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 8
});
};
return (
<div className="space-y-6">
<h1 className="text-3xl font-bold">Address Details</h1>
<div className="card">
<div className="flex flex-col md:flex-row gap-8">
<div className="flex-1">
<h2 className="text-xl font-bold mb-4">Address</h2>
<div className="bg-gray-800 p-4 rounded mb-4">
<div className="hash text-lg break-all mb-2">{address}</div>
<button
onClick={copyToClipboard}
className="btn btn-secondary text-sm flex items-center gap-2"
>
<Copy className="w-4 h-4" />
Copy Address
</button>
</div>
{/* Balance Info */}
<div className="bg-gray-800 p-4 rounded mb-4">
<h3 className="text-lg font-bold mb-3 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-yellow-400" />
Balance
</h3>
{loadingBalance ? (
<div className="text-gray-400 text-sm">Loading balance...</div>
) : balanceData?.balance !== null && balanceData?.balance !== undefined ? (
<>
<div className="text-3xl font-bold text-green-400 mb-2">
{formatBalance(balanceData.balance)} MAZA
</div>
{balanceData.rank && (
<div className="text-sm text-gray-400">
Rank #{balanceData.rank} in rich list
</div>
)}
{balanceData.lastSeen && (
<div className="text-xs text-gray-500 mt-1">
Last seen in block: {balanceData.lastSeen.toLocaleString()}
</div>
)}
</>
) : (
<div className="text-sm text-gray-400">
{balanceData?.message || 'Balance data not available yet'}
{balanceData?.lastScannedBlock && (
<div className="text-xs text-gray-500 mt-1">
Scanner progress: block {balanceData.lastScannedBlock.toLocaleString()}
</div>
)}
</div>
)}
</div>
<div className="bg-blue-900/30 border border-blue-700 text-blue-300 p-4 rounded text-sm">
<p className="mb-2">
<strong>About Balance Data:</strong>
</p>
<ul className="list-disc list-inside space-y-1 text-xs">
<li>Balance is calculated by our blockchain scanner tracking all transaction outputs</li>
<li>Scanner is running in the background and processes blocks continuously</li>
<li>Top addresses appear in the rich list as they're discovered</li>
<li>Full transaction history requires <code className="bg-gray-800 px-1 rounded">txindex=1</code> on the node</li>
</ul>
</div>
</div>
<div className="flex flex-col items-center">
<h3 className="text-lg font-bold mb-4">QR Code</h3>
<div className="bg-white p-4 rounded">
<QRCodeSVG value={address} size={200} />
</div>
</div>
</div>
</div>
<div className="card">
<h2 className="text-xl font-bold mb-4">Transaction History</h2>
<div className="text-gray-400 text-center py-8">
<p className="mb-2">Transaction history for addresses requires txindex to be enabled on the Mazacoin node.</p>
<p className="text-sm">This feature will be available once the node is configured with txindex and fully synced.</p>
</div>
</div>
</div>
);
}
export default AddressDetail;
+187
View File
@@ -0,0 +1,187 @@
import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { ChevronLeft, ChevronRight, Clock, Hash, Layers } from 'lucide-react';
import axios from 'axios';
import { format } from 'date-fns';
const API_URL = process.env.REACT_APP_API_URL || '';
function BlockDetail() {
const { hashOrHeight } = useParams();
const [block, setBlock] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
fetchBlock();
}, [hashOrHeight]);
const fetchBlock = async () => {
setLoading(true);
setError('');
try {
const response = await axios.get(`${API_URL}/api/block/${hashOrHeight}`);
setBlock(response.data);
} catch (err) {
setError(err.response?.data?.error || 'Block not found');
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="loading">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
</div>
);
}
if (error) {
return (
<div className="error">
{error}
</div>
);
}
if (!block) return null;
return (
<div className="space-y-6">
{/* Navigation */}
<div className="flex items-center justify-between">
<Link
to={`/block/${block.height - 1}`}
className="btn btn-secondary flex items-center gap-2"
>
<ChevronLeft className="w-4 h-4" />
Previous Block
</Link>
<h1 className="text-3xl font-bold flex items-center gap-2">
<Layers className="w-8 h-8 text-maza-blue" />
Block #{block.height}
</h1>
<Link
to={`/block/${block.height + 1}`}
className="btn btn-secondary flex items-center gap-2"
>
Next Block
<ChevronRight className="w-4 h-4" />
</Link>
</div>
{/* Block Details */}
<div className="card">
<h2 className="text-xl font-bold mb-4">Block Information</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div className="text-gray-400 text-sm mb-1">Height</div>
<div className="text-lg font-bold">{block.height}</div>
</div>
<div>
<div className="text-gray-400 text-sm mb-1">Timestamp</div>
<div className="text-lg flex items-center gap-2">
<Clock className="w-4 h-4" />
{format(new Date(block.time * 1000), 'PPpp')}
</div>
</div>
<div className="md:col-span-2">
<div className="text-gray-400 text-sm mb-1">Hash</div>
<div className="hash text-lg bg-gray-800 p-3 rounded">{block.hash}</div>
</div>
{block.previousblockhash && (
<div className="md:col-span-2">
<div className="text-gray-400 text-sm mb-1">Previous Block Hash</div>
<Link
to={`/block/${block.previousblockhash}`}
className="hash text-lg bg-gray-800 p-3 rounded hover:bg-gray-700 block transition-colors"
>
{block.previousblockhash}
</Link>
</div>
)}
{block.nextblockhash && (
<div className="md:col-span-2">
<div className="text-gray-400 text-sm mb-1">Next Block Hash</div>
<Link
to={`/block/${block.nextblockhash}`}
className="hash text-lg bg-gray-800 p-3 rounded hover:bg-gray-700 block transition-colors"
>
{block.nextblockhash}
</Link>
</div>
)}
<div>
<div className="text-gray-400 text-sm mb-1">Difficulty</div>
<div className="text-lg">{block.difficulty?.toFixed(4)}</div>
</div>
<div>
<div className="text-gray-400 text-sm mb-1">Size</div>
<div className="text-lg">{(block.size / 1024).toFixed(2)} KB</div>
</div>
<div>
<div className="text-gray-400 text-sm mb-1">Confirmations</div>
<div className="text-lg text-green-400">{block.confirmations}</div>
</div>
<div>
<div className="text-gray-400 text-sm mb-1">Version</div>
<div className="text-lg">{block.version}</div>
</div>
</div>
</div>
{/* Transactions */}
<div className="card">
<h2 className="text-xl font-bold mb-4 flex items-center justify-between">
<span>Transactions ({block.tx?.length || 0})</span>
{block.totalAmount !== undefined && block.totalAmount > 0 && (
<span className="text-lg text-green-400">
Total: {block.totalAmount.toFixed(2)} MAZA
</span>
)}
</h2>
{block.tx && block.tx.length > 0 ? (
<div className="space-y-2">
{block.tx.map((txid, index) => (
<Link
key={txid}
to={`/tx/${txid}`}
className="block p-3 bg-gray-800 hover:bg-gray-700 rounded transition-colors"
>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="text-gray-400 text-sm shrink-0">#{index}</div>
<div className="hash flex-1 truncate">{txid}</div>
</div>
{index === 0 && (
<div className="text-xs text-yellow-400 shrink-0">Coinbase</div>
)}
</div>
</Link>
))}
</div>
) : (
<div className="text-gray-400 text-center py-8">
No transactions in this block
</div>
)}
</div>
</div>
);
}
export default BlockDetail;
+172
View File
@@ -0,0 +1,172 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { Clock, Layers } from 'lucide-react';
import SearchBar from '../components/SearchBar';
import PriceChart from '../components/PriceChart';
import axios from 'axios';
import { formatDistance } from 'date-fns';
import io from 'socket.io-client';
const API_URL = process.env.REACT_APP_API_URL || '';
function Home() {
const [latestBlocks, setLatestBlocks] = useState([]);
const [stats, setStats] = useState(null);
const [loading, setLoading] = useState(true);
const [price, setPrice] = useState(null);
useEffect(() => {
fetchData();
// WebSocket for real-time updates
const socket = io(API_URL);
socket.emit('subscribe:blocks');
socket.on('block:new', (block) => {
setLatestBlocks(prev => [block, ...prev].slice(0, 10));
});
return () => socket.disconnect();
}, []);
const fetchData = async () => {
try {
const [blocksRes, statsRes, priceRes] = await Promise.all([
axios.get(`${API_URL}/api/blocks/latest/3`, { timeout: 30000 }),
axios.get(`${API_URL}/api/stats`, { timeout: 30000 }),
axios.get(`${API_URL}/api/price`, { timeout: 30000 })
]);
// Ensure we got valid data
if (Array.isArray(blocksRes.data)) {
setLatestBlocks(blocksRes.data);
}
if (statsRes.data && typeof statsRes.data === 'object') {
setStats(statsRes.data);
}
if (priceRes.data && typeof priceRes.data === 'object') {
setPrice(priceRes.data);
}
} catch (error) {
console.error('Error fetching data:', error);
// Set empty/fallback data on error
setLatestBlocks([]);
} finally {
setLoading(false);
}
};
return (
<div className="space-y-8">
{/* Hero Section */}
<div className="text-center py-12">
<h1 className="text-5xl font-bold mb-4">Mazacoin Blockchain Explorer</h1>
<p className="text-xl text-gray-400 mb-8">
Explore blocks, transactions, and addresses on the Mazacoin network
</p>
<SearchBar />
</div>
{/* Network Stats */}
{stats && stats.blockHeight !== undefined && (
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
{price && price.btc !== undefined && (
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">MAZA Price</div>
<div className="text-lg font-bold text-yellow-400">
{price.btc?.toFixed(8) || '0.00000000'} BTC
</div>
<div className="text-sm font-bold text-purple-400">
{price.ltc?.toFixed(8) || '0.00000000'} LTC
</div>
<div className="text-sm font-bold text-blue-400">
{price.eth?.toFixed(10) || '0.0000000000'} ETH
</div>
<div className="text-xs text-gray-500 mt-1">
<a href="https://freiexchange.com/market/MAZA/BTC" target="_blank" rel="noreferrer" className="text-maza-blue hover:underline">FreiExchange</a>
</div>
</div>
)}
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Block Height</div>
<div className="text-2xl font-bold text-maza-blue">
{stats.blockHeight?.toLocaleString() || '0'}
</div>
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Difficulty</div>
<div className="text-2xl font-bold">
{stats.difficulty?.toFixed(2) || '0.00'}
</div>
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Network Hashrate</div>
<div className="text-2xl font-bold">
{((stats.networkHashrate || 0) / 1000000).toFixed(2)} MH/s
</div>
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Connections</div>
<div className="text-2xl font-bold text-green-400">
{stats.connections || 0}
</div>
</div>
</div>
)}
{/* Price Chart */}
<PriceChart currency="btc" timeframe="all" />
{/* Latest Blocks */}
<div className="card">
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2">
<Layers className="w-6 h-6 text-maza-blue" />
Latest Blocks
</h2>
{loading ? (
<div className="loading">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
</div>
) : (
<div className="space-y-2">
{latestBlocks.map((block) => (
<Link
key={block.height}
to={`/block/${block.height}`}
className="block p-4 bg-gray-800 hover:bg-gray-700 rounded-lg transition-colors"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="bg-maza-blue/20 text-maza-blue px-3 py-1 rounded-lg font-bold">
{block.height}
</div>
<div className="hash text-gray-400">
{block.hash.substring(0, 16)}...
</div>
</div>
<div className="flex items-center gap-6 text-sm text-gray-400">
<div className="flex items-center gap-1">
<Clock className="w-4 h-4" />
{formatDistance(new Date(block.time * 1000), new Date(), { addSuffix: true })}
</div>
<div>
{block.txCount} {block.txCount === 1 ? 'tx' : 'txs'}
</div>
{block.totalAmount !== undefined && block.totalAmount > 0 && (
<div className="text-green-400 font-semibold">
{block.totalAmount.toFixed(2)} MAZA
</div>
)}
</div>
</div>
</Link>
))}
</div>
)}
</div>
</div>
);
}
export default Home;
+132
View File
@@ -0,0 +1,132 @@
import React, { useState, useEffect } from 'react';
import { Activity, Server, Zap, Clock } from 'lucide-react';
import axios from 'axios';
const API_URL = process.env.REACT_APP_API_URL || '';
function NetworkStats() {
const [stats, setStats] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchStats();
const interval = setInterval(fetchStats, 30000); // Update every 30s
return () => clearInterval(interval);
}, []);
const fetchStats = async () => {
try {
const response = await axios.get(`${API_URL}/api/stats`);
setStats(response.data);
} catch (error) {
console.error('Error fetching stats:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="loading">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
</div>
);
}
if (!stats) {
return <div className="error">Failed to load network statistics</div>;
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold flex items-center gap-2">
<Activity className="w-8 h-8 text-maza-blue" />
Network Statistics
</h1>
<div className="text-sm text-gray-400">
Last updated: {new Date(stats.timestamp).toLocaleTimeString()}
</div>
</div>
{/* Main Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="card">
<div className="flex items-center gap-3 mb-4">
<div className="p-3 bg-maza-blue/20 rounded-lg">
<Server className="w-6 h-6 text-maza-blue" />
</div>
<div>
<div className="text-gray-400 text-sm">Block Height</div>
<div className="text-3xl font-bold">{stats.blockHeight?.toLocaleString() || '0'}</div>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center gap-3 mb-4">
<div className="p-3 bg-purple-500/20 rounded-lg">
<Zap className="w-6 h-6 text-purple-400" />
</div>
<div>
<div className="text-gray-400 text-sm">Network Hashrate</div>
<div className="text-3xl font-bold">
{((stats.networkHashrate || 0) / 1000000).toFixed(2)} MH/s
</div>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center gap-3 mb-4">
<div className="p-3 bg-green-500/20 rounded-lg">
<Activity className="w-6 h-6 text-green-400" />
</div>
<div>
<div className="text-gray-400 text-sm">Active Connections</div>
<div className="text-3xl font-bold text-green-400">{stats.connections}</div>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center gap-3 mb-4">
<div className="p-3 bg-yellow-500/20 rounded-lg">
<Clock className="w-6 h-6 text-yellow-400" />
</div>
<div>
<div className="text-gray-400 text-sm">Difficulty</div>
<div className="text-2xl font-bold">{stats.difficulty?.toFixed(2) || '0.00'}</div>
</div>
</div>
</div>
<div className="card">
<div className="text-gray-400 text-sm mb-1">Protocol Version</div>
<div className="text-2xl font-bold">{stats.protocolVersion}</div>
</div>
<div className="card">
<div className="text-gray-400 text-sm mb-1">Client Version</div>
<div className="text-2xl font-bold">{stats.version}</div>
</div>
</div>
{/* Additional Info */}
<div className="card">
<h2 className="text-xl font-bold mb-4">About Mazacoin</h2>
<div className="text-gray-300 space-y-2">
<p>
Mazacoin (MAZA) is a cryptocurrency designed for the Oglala Lakota Nation.
It is based on the Bitcoin protocol and uses Proof-of-Work consensus.
</p>
<p className="text-sm text-gray-400">
This explorer provides real-time blockchain data directly from Mazacoin nodes.
</p>
</div>
</div>
</div>
);
}
export default NetworkStats;
+248
View File
@@ -0,0 +1,248 @@
import React, { useState, useEffect } from 'react';
import { ComposableMap, Geographies, Geography, Marker, ZoomableGroup } from 'react-simple-maps';
import { MapPin, Globe } from 'lucide-react';
import axios from 'axios';
const API_URL = process.env.REACT_APP_API_URL || '';
// World map TopoJSON URL
const geoUrl = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json";
function NodeMap() {
const [nodes, setNodes] = useState([]);
const [geoNodes, setGeoNodes] = useState([]);
const [loading, setLoading] = useState(true);
const [stats, setStats] = useState({ total: 0, active: 0, countries: {} });
const [price, setPrice] = useState(null);
useEffect(() => {
fetchNodes();
fetchPrice();
const interval = setInterval(() => {
fetchNodes();
fetchPrice();
}, 60000); // Update every minute
return () => clearInterval(interval);
}, []);
const fetchPrice = async () => {
try {
const response = await axios.get(`${API_URL}/api/price`);
setPrice(response.data);
} catch (error) {
console.error('Error fetching price:', error);
}
};
const fetchNodes = async () => {
try {
// Fetch nodes with geolocation from backend API
const response = await axios.get(`${API_URL}/api/nodes`);
const data = response.data;
setNodes(data.nodes || []);
setGeoNodes(data.nodes || []);
setStats({
total: data.total || 0,
active: data.active || 0,
countries: data.countries || {}
});
} catch (error) {
console.error('Error fetching nodes:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="loading">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold flex items-center gap-2">
<MapPin className="w-8 h-8 text-maza-blue" />
Live Node Map
</h1>
<div className="text-sm text-gray-400">
Showing all nodes discovered in the last 24 hours
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
{price && price.btc !== undefined && (
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">MAZA Price</div>
<div className="text-lg font-bold text-yellow-400">
{price.btc?.toFixed(8) || '0.00000000'} BTC
</div>
<div className="text-sm font-bold text-purple-400">
{price.ltc?.toFixed(8) || '0.00000000'} LTC
</div>
<div className="text-sm font-bold text-blue-400">
{price.eth?.toFixed(10) || '0.0000000000'} ETH
</div>
</div>
)}
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Nodes (24h)</div>
<div className="text-3xl font-bold text-yellow-400">{stats.total}</div>
<div className="text-sm text-gray-400 mt-1">
<span className="text-yellow-400 font-semibold">{stats.active} active</span>
</div>
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Mapped Nodes</div>
<div className="text-3xl font-bold text-maza-blue">{geoNodes.length}</div>
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">IP Protocol</div>
<div className="text-sm">
<div className="text-lg font-bold text-blue-400">
IPv4: {geoNodes.filter(n => n.ipVersion === 4).length}
</div>
<div className="text-lg font-bold text-purple-400">
IPv6: {geoNodes.filter(n => n.ipVersion === 6).length}
</div>
</div>
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Countries</div>
<div className="text-3xl font-bold">{Object.keys(stats.countries).length}</div>
</div>
</div>
{/* Legend */}
<div className="card bg-gray-800/50 border border-gray-700">
<div className="flex items-center justify-center gap-8 py-2">
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded-full bg-yellow-400 shadow-lg" style={{ filter: 'drop-shadow(0 0 8px rgba(251, 191, 36, 0.8))' }}></div>
<span className="text-sm font-medium">Active Now ({geoNodes.filter(n => n.isActive).length})</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded-full bg-gray-500 opacity-60"></div>
<span className="text-sm font-medium text-gray-400">Seen in Last 24h ({geoNodes.filter(n => !n.isActive).length})</span>
</div>
</div>
</div>
{/* Map */}
<div className="card">
<div className="h-[600px] rounded-lg overflow-hidden bg-gray-900">
<ComposableMap
projection="geoMercator"
projectionConfig={{
scale: 147
}}
style={{ width: "100%", height: "100%" }}
>
<ZoomableGroup>
<Geographies geography={geoUrl}>
{({ geographies }) =>
geographies.map((geo) => (
<Geography
key={geo.rsmKey}
geography={geo}
fill="#1f2937"
stroke="#374151"
strokeWidth={0.5}
style={{
default: { outline: "none" },
hover: { fill: "#2d3748", outline: "none" },
pressed: { outline: "none" },
}}
/>
))
}
</Geographies>
{geoNodes.map((node, index) => {
const isActive = node.isActive;
const markerColor = isActive ? '#fbbf24' : '#6b7280'; // yellow for active, gray for inactive
const glowColor = isActive ? 'rgba(251, 191, 36, 0.8)' : 'rgba(107, 114, 128, 0.4)';
return (
<Marker key={index} coordinates={[node.lon, node.lat]}>
<g>
<circle
r={6}
fill={markerColor}
stroke={isActive ? '#f59e0b' : '#4b5563'}
strokeWidth={2}
style={{
cursor: "pointer",
filter: `drop-shadow(0 0 8px ${glowColor})`
}}
/>
<circle
r={12}
fill={markerColor}
fillOpacity={0.2}
stroke="none"
style={{ pointerEvents: "none" }}
/>
</g>
<title>
{isActive ? '🟡 ACTIVE' : '⚪ Seen in last 24h'}
{'\n'}{node.city}, {node.country}
{'\n'}IP: {node.ip || node.addr?.split(':')[0]} (IPv{node.ipVersion || '4'})
{node.subver ? `\nClient: ${node.subver}` : ''}
{node.firstSeen ? `\nFirst seen: ${new Date(node.firstSeen).toLocaleString()}` : ''}
{node.lastSeen ? `\nLast seen: ${new Date(node.lastSeen).toLocaleString()}` : ''}
</title>
</Marker>
);
})}
</ZoomableGroup>
</ComposableMap>
</div>
{geoNodes.length === 0 && (
<div className="text-center text-gray-400 py-8">
<p>No nodes could be geolocated yet.</p>
<p className="text-sm mt-2">Geolocation is in progress...</p>
</div>
)}
</div>
{/* Country Distribution */}
<div className="card">
<h2 className="text-xl font-bold mb-4 flex items-center gap-2">
<Globe className="w-6 h-6" />
Node Distribution by Country
</h2>
{Object.keys(stats.countries).length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(stats.countries)
.sort(([, a], [, b]) => b - a)
.map(([country, count]) => (
<div key={country} className="bg-gray-800 p-3 rounded">
<div className="flex items-center justify-between">
<div className="font-medium">{country}</div>
<div className="text-maza-blue font-bold">{count}</div>
</div>
</div>
))}
</div>
) : (
<div className="text-gray-400 text-center py-8">
No country data available yet.
</div>
)}
</div>
</div>
);
}
export default NodeMap;
+214
View File
@@ -0,0 +1,214 @@
import React, { useState, useEffect } from 'react';
import { Trophy, TrendingUp, AlertCircle } from 'lucide-react';
import axios from 'axios';
const API_URL = process.env.REACT_APP_API_URL || '';
function RichList() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchRichList();
const interval = setInterval(fetchRichList, 60000); // Update every minute
return () => clearInterval(interval);
}, []);
const fetchRichList = async () => {
try {
setError(null);
const response = await axios.get(`${API_URL}/api/richlist/100`);
setData(response.data);
} catch (error) {
console.error('Error fetching rich list:', error);
setError('Failed to load rich list');
} finally {
setLoading(false);
}
};
const formatBalance = (balance) => {
return balance.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 8
});
};
const formatBlockNumber = (block) => {
return block ? block.toLocaleString() : 'Unknown';
};
const getTotalBalance = () => {
if (!data || !data.addresses) return 0;
return data.addresses.reduce((sum, addr) => sum + addr.balance, 0);
};
const getRankColor = (rank) => {
if (rank === 1) return 'text-yellow-400'; // Gold
if (rank === 2) return 'text-gray-300'; // Silver
if (rank === 3) return 'text-yellow-600'; // Bronze
return 'text-gray-400';
};
const getRankIcon = (rank) => {
if (rank <= 3) {
return <Trophy className={`w-5 h-5 ${getRankColor(rank)}`} />;
}
return null;
};
if (loading) {
return (
<div className="loading">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
</div>
);
}
if (error) {
return (
<div className="space-y-6">
<h1 className="text-3xl font-bold flex items-center gap-2">
<TrendingUp className="w-8 h-8 text-maza-blue" />
Top 100 Rich List
</h1>
<div className="card bg-red-900/20 border border-red-500/50">
<div className="flex items-center gap-3 text-red-400">
<AlertCircle className="w-6 h-6" />
<p>{error}</p>
</div>
</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between flex-wrap gap-4">
<h1 className="text-3xl font-bold flex items-center gap-2">
<TrendingUp className="w-8 h-8 text-maza-blue" />
Top 100 Rich List
</h1>
{data?.isScanning && (
<div className="text-sm text-yellow-400 flex items-center gap-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-yellow-400"></div>
Scanning blockchain...
</div>
)}
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Total Addresses</div>
<div className="text-3xl font-bold text-yellow-400">{data?.totalAddresses || 0}</div>
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Last Scanned Block</div>
<div className="text-2xl font-bold text-maza-blue">{formatBlockNumber(data?.lastScannedBlock)}</div>
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Top 100 Total Balance</div>
<div className="text-2xl font-bold text-green-400">{formatBalance(getTotalBalance())} MAZA</div>
</div>
<div className="stat-card">
<div className="text-gray-400 text-sm mb-1">Status</div>
<div className={`text-xl font-bold ${data?.isScanning ? 'text-yellow-400' : 'text-green-400'}`}>
{data?.isScanning ? 'Scanning...' : 'Up to date'}
</div>
</div>
</div>
{/* Info Banner */}
<div className="card bg-blue-900/20 border border-blue-500/50">
<div className="flex items-start gap-3 text-blue-300 text-sm">
<AlertCircle className="w-5 h-5 flex-shrink-0 mt-0.5" />
<div>
<p className="font-semibold mb-1">About the Rich List</p>
<p>
This list shows the top 100 addresses by balance. Balances are calculated by tracking
transaction outputs as the blockchain is scanned. The scanner runs in the background
and processes blocks incrementally. Large exchanges and mining pools typically appear
at the top of this list.
</p>
</div>
</div>
</div>
{/* Rich List Table */}
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-800 border-b border-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Rank
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Address
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase tracking-wider">
Balance (MAZA)
</th>
<th className="px-6 py-3 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">
Last Seen Block
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700">
{data?.addresses?.map((addr) => (
<tr
key={addr.address}
className="hover:bg-gray-800/50 transition-colors"
>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center gap-2">
{getRankIcon(addr.rank)}
<span className={`text-lg font-bold ${getRankColor(addr.rank)}`}>
{addr.rank}
</span>
</div>
</td>
<td className="px-6 py-4">
<a
href={`/address/${addr.address}`}
className="text-maza-blue hover:text-blue-300 font-mono text-sm break-all"
>
{addr.address}
</a>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right">
<span className="text-green-400 font-bold">
{formatBalance(addr.balance)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-center text-gray-400 text-sm">
{formatBlockNumber(addr.lastSeen)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{!data?.addresses || data.addresses.length === 0 ? (
<div className="card">
<div className="text-center text-gray-400 py-12">
<AlertCircle className="w-12 h-12 mx-auto mb-4 text-gray-500" />
<p className="text-lg mb-2">No data available yet</p>
<p className="text-sm">
The blockchain scanner is starting up. Please check back in a few minutes.
</p>
</div>
</div>
) : null}
</div>
);
}
export default RichList;
+218
View File
@@ -0,0 +1,218 @@
import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { ArrowRight, Clock } from 'lucide-react';
import axios from 'axios';
import { format } from 'date-fns';
const API_URL = process.env.REACT_APP_API_URL || '';
function TransactionDetail() {
const { txid } = useParams();
const [tx, setTx] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
fetchTransaction();
}, [txid]);
const fetchTransaction = async () => {
setLoading(true);
setError('');
try {
const response = await axios.get(`${API_URL}/api/tx/${txid}`);
setTx(response.data);
} catch (err) {
setError(err.response?.data?.error || 'Transaction not found');
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="loading">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-maza-blue"></div>
</div>
);
}
if (error) {
return <div className="error">{error}</div>;
}
if (!tx) return null;
// Use backend-calculated totals if available, otherwise calculate
const totalInput = tx.totalInput !== undefined ? tx.totalInput :
tx.vin?.reduce((sum, input) => sum + (input.value || 0), 0) || 0;
const totalOutput = tx.totalOutput !== undefined ? tx.totalOutput :
tx.vout?.reduce((sum, output) => sum + (output.value || 0), 0) || 0;
const fee = tx.fees !== undefined ? tx.fees : (totalInput - totalOutput);
return (
<div className="space-y-6">
<h1 className="text-3xl font-bold">Transaction Details</h1>
{/* Transaction Info */}
<div className="card">
<h2 className="text-xl font-bold mb-4">Transaction Information</h2>
<div className="grid grid-cols-1 gap-6">
<div>
<div className="text-gray-400 text-sm mb-1">Transaction ID</div>
<div className="hash text-lg bg-gray-800 p-3 rounded">{tx.txid}</div>
</div>
{tx.blockhash && (
<div>
<div className="text-gray-400 text-sm mb-1">Block Hash</div>
<Link
to={`/block/${tx.blockhash}`}
className="hash text-lg bg-gray-800 p-3 rounded hover:bg-gray-700 block transition-colors"
>
{tx.blockhash}
</Link>
</div>
)}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{tx.confirmations !== undefined && (
<div>
<div className="text-gray-400 text-sm mb-1">Confirmations</div>
<div className="text-lg text-green-400">{tx.confirmations}</div>
</div>
)}
{tx.time && (
<div>
<div className="text-gray-400 text-sm mb-1">Time</div>
<div className="text-sm flex items-center gap-1">
<Clock className="w-4 h-4" />
{format(new Date(tx.time * 1000), 'PPpp')}
</div>
</div>
)}
{tx.size && (
<div>
<div className="text-gray-400 text-sm mb-1">Size</div>
<div className="text-lg">{tx.size} bytes</div>
</div>
)}
{fee > 0 && (
<div>
<div className="text-gray-400 text-sm mb-1">Fee</div>
<div className="text-lg">{fee.toFixed(8)} MAZA</div>
</div>
)}
</div>
</div>
</div>
{/* Inputs and Outputs */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Inputs */}
<div className="card">
<h3 className="text-lg font-bold mb-4">
Inputs ({tx.vin?.length || 0})
</h3>
{tx.vin && tx.vin.length > 0 ? (
<>
<div className="space-y-3 mb-4">
{tx.vin.map((input, index) => (
<div key={index} className="bg-gray-800 p-3 rounded">
{input.coinbase || input.isCoinbase ? (
<div className="text-sm text-gray-400">Coinbase (Newly Generated Coins)</div>
) : (
<>
{input.txid && (
<Link
to={`/tx/${input.txid}`}
className="hash text-xs text-maza-blue hover:underline block mb-1"
>
{input.txid}:{input.vout}
</Link>
)}
{input.value !== undefined && input.value !== null ? (
<>
<div className="text-lg font-bold">{input.value.toFixed(8)} MAZA</div>
{input.address && input.address !== 'Unknown' && (
<Link
to={`/address/${input.address}`}
className="hash text-xs text-maza-blue hover:underline block mt-1"
>
{input.address}
</Link>
)}
</>
) : input.error ? (
<div className="text-xs text-yellow-400">{input.error}</div>
) : (
<div className="text-xs text-gray-500">Amount unknown</div>
)}
</>
)}
</div>
))}
</div>
{totalInput > 0 && (
<div className="pt-3 border-t border-gray-700">
<div className="flex justify-between items-center">
<span className="text-gray-400">Total Input:</span>
<span className="text-xl font-bold text-green-400">{totalInput.toFixed(8)} MAZA</span>
</div>
</div>
)}
</>
) : (
<div className="text-gray-400 text-center py-4">No inputs</div>
)}
</div>
{/* Outputs */}
<div className="card">
<h3 className="text-lg font-bold mb-4">
Outputs ({tx.vout?.length || 0})
</h3>
{tx.vout && tx.vout.length > 0 ? (
<>
<div className="space-y-3 mb-4">
{tx.vout.map((output, index) => (
<div key={index} className="bg-gray-800 p-3 rounded">
<div className="text-lg font-bold mb-1">{output.value.toFixed(8)} MAZA</div>
{output.scriptPubKey?.addresses?.map((address, i) => (
<Link
key={i}
to={`/address/${address}`}
className="hash text-xs text-maza-blue hover:underline block"
>
{address}
</Link>
))}
</div>
))}
</div>
{totalOutput > 0 && (
<div className="pt-3 border-t border-gray-700">
<div className="flex justify-between items-center">
<span className="text-gray-400">Total Output:</span>
<span className="text-xl font-bold text-blue-400">{totalOutput.toFixed(8)} MAZA</span>
</div>
</div>
)}
</>
) : (
<div className="text-gray-400 text-center py-4">No outputs</div>
)}
</div>
</div>
</div>
);
}
export default TransactionDetail;
+16
View File
@@ -0,0 +1,16 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {
colors: {
'maza-blue': '#1e40af',
'maza-dark': '#0f172a',
'maza-gray': '#1e293b',
}
},
},
plugins: [],
}