Skip to content

Commit d444305

Browse files
committed
Add block exploration
1 parent 3ed0110 commit d444305

File tree

14 files changed

+1218
-230
lines changed

14 files changed

+1218
-230
lines changed

cmd/main.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func main() {
8484
DHTProtocolID: dhtProtocolID,
8585
}
8686

87-
node, err := p2p.NewP2PNode(ctx, log, config)
87+
node, err := p2p.NewNode(ctx, log, config)
8888
if err != nil {
8989
panic(err)
9090
}
@@ -117,7 +117,7 @@ func main() {
117117

118118
// Capture timestamp once for consistent storage
119119
receivedAt := time.Now()
120-
120+
121121
// Store parsed message in appropriate table
122122
var storeErr error
123123
switch parsedMsg.Type {
@@ -137,9 +137,28 @@ func main() {
137137
Height: blockMsg.Height,
138138
DataHubURL: blockMsg.DataHubURL,
139139
PeerID: blockMsg.PeerID,
140+
Header: blockMsg.Header,
140141
ReceivedAt: receivedAt,
141142
}).Error
142143

144+
// Parse and store block header if available
145+
if storeErr == nil && blockMsg.Header != "" {
146+
blockHeader, parseErr := parser.ParseBlockHeader(blockMsg.Header, parsedMsg.Network, receivedAt)
147+
if parseErr != nil {
148+
log.Errorf("Failed to parse block header: %v", parseErr)
149+
} else {
150+
// Update height from the block message
151+
blockHeader.Height = blockMsg.Height
152+
blockHeader.Hash = blockMsg.Hash
153+
154+
if err := db.Create(blockHeader).Error; err != nil {
155+
log.Errorf("Failed to store block header: %v", err)
156+
} else {
157+
log.Infof("Stored block header for block %s at height %d", blockMsg.Hash, blockMsg.Height)
158+
}
159+
}
160+
}
161+
143162
case parser.TypeMiningOn:
144163
miningMsg := parsedMsg.Data.(p2p.MiningOnMessage)
145164
storeErr = db.Create(&model.MiningOn{

frontend-react/src/App.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Dashboard } from './components/Dashboard';
44
import Peers from './components/Peers';
55
import PeerDetail from './components/PeerDetail';
66
import Stats from './components/Stats';
7+
import { BlockExplorer } from './components/BlockExplorer';
78
import './App.css';
89

910
function Navigation() {
@@ -70,6 +71,16 @@ function Navigation() {
7071
>
7172
Peers
7273
</Link>
74+
<Link
75+
to="/blocks"
76+
className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${
77+
location.pathname === '/blocks'
78+
? 'border-blue-500 text-gray-900'
79+
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'
80+
}`}
81+
>
82+
Blocks
83+
</Link>
7384
</div>
7485
</div>
7586

@@ -137,6 +148,16 @@ function Navigation() {
137148
>
138149
Peers
139150
</Link>
151+
<Link
152+
to="/blocks"
153+
className={`block px-3 py-2 rounded-md text-base font-medium ${
154+
location.pathname === '/blocks'
155+
? 'bg-blue-50 border-blue-500 text-blue-700'
156+
: 'text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800'
157+
} border-l-4`}
158+
>
159+
Blocks
160+
</Link>
140161
</div>
141162
</div>
142163
</nav>
@@ -155,6 +176,7 @@ function App() {
155176
<Route path="/stats" element={<Stats />} />
156177
<Route path="/peers" element={<Peers />} />
157178
<Route path="/peers/:peerID" element={<PeerDetail />} />
179+
<Route path="/blocks" element={<BlockExplorer />} />
158180
<Route path="*" element={<Navigate to="/" replace />} />
159181
</Routes>
160182
</div>
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { BlockHeader } from '../types/Message';
3+
4+
interface BlockDetailsModalProps {
5+
blockHeader: BlockHeader | null;
6+
isOpen: boolean;
7+
onClose: () => void;
8+
}
9+
10+
export const BlockDetailsModal: React.FC<BlockDetailsModalProps> = ({ blockHeader, isOpen, onClose }) => {
11+
const [coinbaseData, setCoinbaseData] = useState<any>(null);
12+
const [loading, setLoading] = useState(false);
13+
14+
useEffect(() => {
15+
if (blockHeader && isOpen) {
16+
// TODO: Fetch coinbase transaction data when API is available
17+
// For now, we'll display the block header information
18+
setCoinbaseData(null);
19+
}
20+
}, [blockHeader, isOpen]);
21+
22+
if (!isOpen || !blockHeader) return null;
23+
24+
const formatTimestamp = (timestamp: number) => {
25+
const date = new Date(timestamp * 1000);
26+
return date.toLocaleString();
27+
};
28+
29+
const getTimeSince = (timestamp: number) => {
30+
const now = Date.now() / 1000;
31+
const diff = now - timestamp;
32+
33+
if (diff < 60) return 'Just now';
34+
if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
35+
if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
36+
return `${Math.floor(diff / 86400)} days ago`;
37+
};
38+
39+
const calculateDifficulty = (bits: number) => {
40+
// Convert bits to difficulty
41+
const nShift = (bits >> 24) & 0xff;
42+
let dDiff = 0x0000ffff / (bits & 0x00ffffff);
43+
let nShiftAmount = 256 - nShift - 32;
44+
45+
while (nShiftAmount > 0) {
46+
dDiff = dDiff * 256.0;
47+
nShiftAmount -= 8;
48+
}
49+
50+
return dDiff.toExponential(2);
51+
};
52+
53+
const getNetworkColor = (network: string) => {
54+
switch (network) {
55+
case 'mainnet': return 'from-green-400 to-green-600';
56+
case 'testnet': return 'from-blue-400 to-blue-600';
57+
case 'teratestnet': return 'from-indigo-400 to-indigo-600';
58+
default: return 'from-gray-400 to-gray-600';
59+
}
60+
};
61+
62+
return (
63+
<div className="fixed inset-0 z-50 overflow-y-auto">
64+
<div className="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
65+
{/* Background overlay */}
66+
<div className="fixed inset-0 transition-opacity" onClick={onClose}>
67+
<div className="absolute inset-0 bg-gray-900 opacity-75"></div>
68+
</div>
69+
70+
{/* Modal panel */}
71+
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full">
72+
{/* Header */}
73+
<div className={`bg-gradient-to-r ${getNetworkColor(blockHeader.Network)} px-6 py-4`}>
74+
<div className="flex items-center justify-between">
75+
<div className="flex items-center gap-4">
76+
<div className="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center">
77+
<span className="text-2xl">⛓️</span>
78+
</div>
79+
<div>
80+
<h3 className="text-2xl font-bold text-white">Block #{blockHeader.Height.toLocaleString()}</h3>
81+
<p className="text-white/80">{blockHeader.Network}</p>
82+
</div>
83+
</div>
84+
<button
85+
onClick={onClose}
86+
className="text-white/80 hover:text-white transition-colors"
87+
>
88+
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
89+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
90+
</svg>
91+
</button>
92+
</div>
93+
</div>
94+
95+
{/* Content */}
96+
<div className="bg-gray-50 px-6 py-6">
97+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
98+
{/* Block Information */}
99+
<div className="bg-white rounded-xl p-6 shadow-sm">
100+
<h4 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
101+
<svg className="w-5 h-5 text-[#1B1EA9]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
102+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
103+
</svg>
104+
Block Header
105+
</h4>
106+
107+
<div className="space-y-3">
108+
<div>
109+
<label className="text-sm text-gray-500">Hash</label>
110+
<div className="flex items-center gap-2">
111+
<p className="font-mono text-sm text-gray-900 break-all">{blockHeader.Hash}</p>
112+
<button
113+
onClick={() => navigator.clipboard.writeText(blockHeader.Hash)}
114+
className="flex-shrink-0 text-gray-400 hover:text-gray-600"
115+
>
116+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
117+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
118+
</svg>
119+
</button>
120+
</div>
121+
</div>
122+
123+
<div>
124+
<label className="text-sm text-gray-500">Previous Hash</label>
125+
<p className="font-mono text-sm text-gray-900 break-all">{blockHeader.PreviousHash}</p>
126+
</div>
127+
128+
<div>
129+
<label className="text-sm text-gray-500">Merkle Root</label>
130+
<p className="font-mono text-sm text-gray-900 break-all">{blockHeader.MerkleRoot}</p>
131+
</div>
132+
133+
<div className="grid grid-cols-2 gap-4">
134+
<div>
135+
<label className="text-sm text-gray-500">Version</label>
136+
<p className="text-gray-900">{blockHeader.Version}</p>
137+
</div>
138+
<div>
139+
<label className="text-sm text-gray-500">Nonce</label>
140+
<p className="text-gray-900">{blockHeader.Nonce.toLocaleString()}</p>
141+
</div>
142+
</div>
143+
</div>
144+
</div>
145+
146+
{/* Mining Information */}
147+
<div className="bg-white rounded-xl p-6 shadow-sm">
148+
<h4 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
149+
<svg className="w-5 h-5 text-[#FF2DAF]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
150+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
151+
</svg>
152+
Mining Details
153+
</h4>
154+
155+
<div className="space-y-3">
156+
<div>
157+
<label className="text-sm text-gray-500">Timestamp</label>
158+
<p className="text-gray-900">{formatTimestamp(blockHeader.Timestamp)}</p>
159+
<p className="text-sm text-gray-600">{getTimeSince(blockHeader.Timestamp)}</p>
160+
</div>
161+
162+
<div>
163+
<label className="text-sm text-gray-500">Difficulty</label>
164+
<p className="text-gray-900">{calculateDifficulty(blockHeader.Bits)}</p>
165+
<p className="text-sm text-gray-600">Bits: 0x{blockHeader.Bits.toString(16)}</p>
166+
</div>
167+
168+
<div>
169+
<label className="text-sm text-gray-500">Received At</label>
170+
<p className="text-gray-900">{new Date(blockHeader.ReceivedAt).toLocaleString()}</p>
171+
</div>
172+
</div>
173+
</div>
174+
</div>
175+
176+
{/* Coinbase Transaction Section */}
177+
<div className="mt-6 bg-white rounded-xl p-6 shadow-sm">
178+
<h4 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
179+
<svg className="w-5 h-5 text-[#1B1EA9]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
180+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
181+
</svg>
182+
Coinbase Transaction
183+
</h4>
184+
185+
{loading ? (
186+
<div className="flex items-center justify-center py-8">
187+
<div className="animate-spin rounded-full h-8 w-8 border-2 border-[#1B1EA9] border-t-transparent"></div>
188+
</div>
189+
) : coinbaseData ? (
190+
<div className="space-y-3">
191+
{/* Coinbase data will be displayed here when API is available */}
192+
<div className="text-gray-600">Coinbase transaction details will be available soon.</div>
193+
</div>
194+
) : (
195+
<div className="text-center py-8">
196+
<div className="inline-flex items-center justify-center w-16 h-16 bg-gray-100 rounded-full mb-4">
197+
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
198+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
199+
</svg>
200+
</div>
201+
<p className="text-gray-600">Coinbase transaction data not yet available</p>
202+
<p className="text-sm text-gray-500 mt-2">This feature requires additional API endpoints</p>
203+
</div>
204+
)}
205+
</div>
206+
</div>
207+
208+
{/* Footer */}
209+
<div className="bg-gray-100 px-6 py-4 flex justify-end">
210+
<button
211+
onClick={onClose}
212+
className="px-6 py-2 bg-gradient-to-r from-[#1B1EA9] to-[#FF2DAF] text-white rounded-lg hover:from-[#1515A0] hover:to-[#FF1FA0] transition-all"
213+
>
214+
Close
215+
</button>
216+
</div>
217+
</div>
218+
</div>
219+
</div>
220+
);
221+
};

0 commit comments

Comments
 (0)